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

Binary file not shown.

View File

@ -6338,26 +6338,31 @@ Sorry for the inconvenience.";
"VoiceChat.PinVideo" = "Pin Video"; "VoiceChat.PinVideo" = "Pin Video";
"VoiceChat.UnpinVideo" = "Unpin Video"; "VoiceChat.UnpinVideo" = "Unpin Video";
"Notification.VoiceChatScheduled" = "Voice chat scheduled"; "Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
"VoiceChat.EditStartTime" = "Edit Start Time";
"VoiceChat.StartsIn" = "Starts in"; "VoiceChat.StartsIn" = "Starts in";
"VoiceChat.LateBy" = "Late by";
"VoiceChat.StartNow" = "Start Now";
"VoiceChat.SetReminder" = "Set Reminder"; "VoiceChat.SetReminder" = "Set Reminder";
"VoiceChat.CancelReminder" = "Cancel Reminder"; "VoiceChat.CancelReminder" = "Cancel Reminder";
"VoiceChat.ShareShort" = "share"; "VoiceChat.ShareShort" = "share";
"VoiceChat.TapToEditTitle" = "Tap to edit title";
"ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat"; "ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat";
"ScheduleVoiceChat.Title" = "Schedule Voice Chat"; "ScheduleVoiceChat.Title" = "Schedule Voice Chat";
"ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@."; "ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@.";
"ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@."; "ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@.";
"ScheduleVoiceChat.ScheduleToday" = "Remind today at %@"; "ScheduleVoiceChat.ScheduleToday" = "Start today at %@";
"ScheduleVoiceChat.ScheduleTomorrow" = "Remind tomorrow at %@"; "ScheduleVoiceChat.ScheduleTomorrow" = "Start tomorrow at %@";
"ScheduleVoiceChat.ScheduleOn" = "Remind on %@ at %@"; "ScheduleVoiceChat.ScheduleOn" = "Start on %@ at %@";
"VoiceChat.ScheduledTitle" = "Scheduled Voice Chat";
"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat"; "Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat";
"Conversation.ScheduledVoiceChatStartsInShort" = "Voice chat starts %@"; "Conversation.ScheduledVoiceChatStartsOn" = "Voice chat starts %@";
"Conversation.ScheduledVoiceChatStartsInShort" = "Starts %@"; "Conversation.ScheduledVoiceChatStartsOnShort" = "Starts %@";

View File

@ -736,6 +736,7 @@ public protocol AccountContext: class {
func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError> func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex) func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
func scheduleGroupCall(peerId: PeerId)
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall)
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
} }

View File

@ -17,6 +17,11 @@ public enum JoinGroupCallManagerResult {
case alreadyInProgress(PeerId?) case alreadyInProgress(PeerId?)
} }
public enum RequestScheduleGroupCallResult {
case success
case alreadyInProgress(PeerId?)
}
public struct CallAuxiliaryServer { public struct CallAuxiliaryServer {
public enum Connection { public enum Connection {
case stun case stun
@ -181,6 +186,7 @@ public struct PresentationGroupCallState: Equatable {
public var recordingStartTimestamp: Int32? public var recordingStartTimestamp: Int32?
public var title: String? public var title: String?
public var raisedHand: Bool public var raisedHand: Bool
public var scheduleTimestamp: Int32?
public init( public init(
myPeerId: PeerId, myPeerId: PeerId,
@ -191,7 +197,8 @@ public struct PresentationGroupCallState: Equatable {
defaultParticipantMuteState: DefaultParticipantMuteState?, defaultParticipantMuteState: DefaultParticipantMuteState?,
recordingStartTimestamp: Int32?, recordingStartTimestamp: Int32?,
title: String?, title: String?,
raisedHand: Bool raisedHand: Bool,
scheduleTimestamp: Int32?
) { ) {
self.myPeerId = myPeerId self.myPeerId = myPeerId
self.networkState = networkState self.networkState = networkState
@ -202,6 +209,7 @@ public struct PresentationGroupCallState: Equatable {
self.recordingStartTimestamp = recordingStartTimestamp self.recordingStartTimestamp = recordingStartTimestamp
self.title = title self.title = title
self.raisedHand = raisedHand self.raisedHand = raisedHand
self.scheduleTimestamp = scheduleTimestamp
} }
} }
@ -299,6 +307,8 @@ public protocol PresentationGroupCall: class {
var isVideo: Bool { get } var isVideo: Bool { get }
var schedulePending: Bool { get }
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get } var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
var canBeRemoved: Signal<Bool, NoError> { get } var canBeRemoved: Signal<Bool, NoError> { get }
@ -313,6 +323,9 @@ public protocol PresentationGroupCall: class {
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get } var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
var reconnectedAsEvents: Signal<Peer, NoError> { get } var reconnectedAsEvents: Signal<Peer, NoError> { get }
func schedule(timestamp: Int32)
func startScheduled()
func reconnect(with invite: String) func reconnect(with invite: String)
func reconnect(as peerId: PeerId) func reconnect(as peerId: PeerId)
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
@ -355,4 +368,5 @@ public protocol PresentationCallManager: class {
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
} }

View File

@ -518,7 +518,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else { } else {
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
} }
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser { if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)" result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
} }
@ -552,7 +552,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else { } else {
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
} }
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser { if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)" result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
} }
@ -958,7 +958,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var hideAuthor = false var hideAuthor = false
switch contentPeer { switch contentPeer {
case let .chat(itemPeer): case let .chat(itemPeer):
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup) var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText { if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
initialHideAuthor = true initialHideAuthor = true

View File

@ -46,7 +46,7 @@ private func messageGroupType(messages: [Message]) -> MessageGroupType {
return currentType return currentType
} }
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) { public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) {
let peer: Peer? let peer: Peer?
let message = messages.last let message = messages.last
@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
} }
default: default:
hideAuthor = true hideAuthor = true
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) { if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
messageText = text messageText = text
} }
} }
case _ as TelegramMediaExpiredContent: case _ as TelegramMediaExpiredContent:
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) { if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
messageText = text messageText = text
} }
case let poll as TelegramMediaPoll: case let poll as TelegramMediaPoll:

View File

@ -569,12 +569,15 @@ final class ContextActionsContainerNode: ASDisplayNode {
} }
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) { func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let additionalActionsNode = self.additionalActionsNode else { guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
return return
} }
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true) transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false) additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
} }
} }

View File

@ -1561,11 +1561,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
} }
} }
} }
if let previousActionsContainerNode = previousActionsContainerNode { if let previousActionsContainerNode = previousActionsContainerNode {
if transition.isAnimated { if transition.isAnimated {
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions { if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true {
var initialFrame = self.actionsContainerNode.frame var initialFrame = self.actionsContainerNode.frame
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height) let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
@ -1773,6 +1772,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)? public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
public var dismissed: (() -> Void)? public var dismissed: (() -> Void)?
public var useComplexItemsTransitionAnimation = false
private var shouldBeDismissedDisposable: Disposable? private var shouldBeDismissedDisposable: Disposable?
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) { public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) {

View File

@ -383,7 +383,12 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U
return tintedImage return tintedImage
} }
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat]) -> UIImage? { public enum GradientImageDirection {
case vertical
case horizontal
}
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? {
guard colors.count == locations.count else { guard colors.count == locations.count else {
return nil return nil
} }
@ -395,7 +400,7 @@ public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [C
var locations = locations var locations = locations
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
} }
let image = UIGraphicsGetImageFromCurrentImageContext()! let image = UIGraphicsGetImageFromCurrentImageContext()!

View File

@ -907,7 +907,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var generalMessageContentKind: MessageContentKind? var generalMessageContentKind: MessageContentKind?
for message in messages { for message in messages {
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
if generalMessageContentKind == nil || generalMessageContentKind == currentKind { if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
generalMessageContentKind = currentKind generalMessageContentKind = currentKind
} else { } else {
@ -1056,7 +1056,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var messageContentKinds = Set<MessageContentKindKey>() var messageContentKinds = Set<MessageContentKindKey>()
for message in messages { for message in messages {
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
if beganContentKindScanning && currentKind != generalMessageContentKind { if beganContentKindScanning && currentKind != generalMessageContentKind {
generalMessageContentKind = nil generalMessageContentKind = nil
} else if !beganContentKindScanning || currentKind == generalMessageContentKind { } else if !beganContentKindScanning || currentKind == generalMessageContentKind {

View File

@ -145,6 +145,12 @@ open class ManagedAnimationNode: ASDisplayNode {
} }
} }
public var scale: CGFloat = 1.0 {
didSet {
self.imageNode.transform = CATransform3DMakeScale(self.scale, self.scale, 1.0)
}
}
public init(size: CGSize) { public init(size: CGSize) {
self.intrinsicSize = size self.intrinsicSize = size
@ -286,4 +292,11 @@ open class ManagedAnimationNode: ASDisplayNode {
self.didTryAdvancingState = false self.didTryAdvancingState = false
self.updateAnimation() self.updateAnimation()
} }
open override func layout() {
super.layout()
self.imageNode.bounds = self.bounds
self.imageNode.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
}
} }

View File

@ -135,19 +135,21 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String let text: String
var actions: [TextAlertAction] = [ var actions: [TextAlertAction] = []
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]
switch error { switch error {
case .limitExceeded: case .limitExceeded:
text = presentationData.strings.Login_CodeFloodError text = presentationData.strings.Login_CodeFloodError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .invalidPhoneNumber: case .invalidPhoneNumber:
text = presentationData.strings.Login_InvalidPhoneError text = presentationData.strings.Login_InvalidPhoneError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .phoneNumberOccupied: case .phoneNumberOccupied:
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0 text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .phoneBanned: case .phoneBanned:
text = presentationData.strings.Login_PhoneBannedError text = presentationData.strings.Login_PhoneBannedError
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -162,6 +164,7 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
})) }))
case .generic: case .generic:
text = presentationData.strings.Login_UnknownError text = presentationData.strings.Login_UnknownError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
} }
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root)) strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))

View File

@ -3,8 +3,6 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
private let textFont: UIFont = Font.regular(16.0)
public final class SolidRoundedButtonTheme { public final class SolidRoundedButtonTheme {
public let backgroundColor: UIColor public let backgroundColor: UIColor
public let foregroundColor: UIColor public let foregroundColor: UIColor

View File

@ -241,7 +241,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId) let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0 var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0
text = foldLineBreaks(text) text = foldLineBreaks(text)
@ -288,7 +289,6 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))

View File

@ -333,7 +333,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 { if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 {
panelData = nil panelData = nil
} else { } else {
panelData = currentGroupCall != nil || availableState?.participantCount == 0 ? nil : availableState panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil) ? nil : availableState
} }
let wasEmpty = strongSelf.groupCallPanelData == nil let wasEmpty = strongSelf.groupCallPanelData == nil
@ -406,7 +406,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
strongSelf.joinGroupCall( strongSelf.joinGroupCall(
peerId: groupCallPanelData.peerId, peerId: groupCallPanelData.peerId,
invite: nil, invite: nil,
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title) activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribed: false)
) )
}) })
if let navigationBar = self.navigationBar { if let navigationBar = self.navigationBar {

View File

@ -41,6 +41,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
case accept case accept
case end case end
case cancel case cancel
case share
} }
var appearance: Appearance var appearance: Appearance
@ -254,6 +255,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel)) context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath() context.strokePath()
}) })
case .share:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: imageColor)
} }
if let image = image { if let image = image {

View File

@ -15,11 +15,20 @@ private let blue = UIColor(rgb: 0x0078ff)
private let lightBlue = UIColor(rgb: 0x59c7f8) private let lightBlue = UIColor(rgb: 0x59c7f8)
private let green = UIColor(rgb: 0x33c659) private let green = UIColor(rgb: 0x33c659)
private let activeBlue = UIColor(rgb: 0x00a0b9) private let activeBlue = UIColor(rgb: 0x00a0b9)
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
private class CallStatusBarBackgroundNode: ASDisplayNode { private class CallStatusBarBackgroundNode: ASDisplayNode {
enum State {
case connecting
case cantSpeak
case active
case speaking
}
private let foregroundView: UIView private let foregroundView: UIView
private let foregroundGradientLayer: CAGradientLayer private let foregroundGradientLayer: CAGradientLayer
private let maskCurveView: VoiceCurveView private let maskCurveView: VoiceCurveView
private let initialTimestamp = CACurrentMediaTime()
var audioLevel: Float = 0.0 { var audioLevel: Float = 0.0 {
didSet { didSet {
@ -35,9 +44,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
} }
} }
var speaking: Bool? = nil { var state: State = .connecting {
didSet { didSet {
if self.speaking != oldValue { if self.state != oldValue {
self.updateGradientColors() self.updateGradientColors()
} }
} }
@ -46,13 +55,26 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
private func updateGradientColors() { private func updateGradientColors() {
let initialColors = self.foregroundGradientLayer.colors let initialColors = self.foregroundGradientLayer.colors
let targetColors: [CGColor] let targetColors: [CGColor]
if let speaking = self.speaking { switch self.state {
targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor] case .connecting:
} else { targetColors = [connectingColor.cgColor, connectingColor.cgColor]
targetColors = [connectingColor.cgColor, connectingColor.cgColor] case .active:
targetColors = [blue.cgColor, lightBlue.cgColor]
case .speaking:
targetColors = [green.cgColor, activeBlue.cgColor]
case .cantSpeak:
targetColors = [purple.cgColor, pink.cgColor]
}
if CACurrentMediaTime() - self.initialTimestamp > 0.1 {
self.foregroundGradientLayer.colors = targetColors
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.foregroundGradientLayer.colors = targetColors
CATransaction.commit()
} }
self.foregroundGradientLayer.colors = targetColors
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
} }
private let hierarchyTrackingNode: HierarchyTrackingNode private let hierarchyTrackingNode: HierarchyTrackingNode
@ -177,6 +199,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
private var currentCallState: PresentationCallState? private var currentCallState: PresentationCallState?
private var currentGroupCallState: PresentationGroupCallSummaryState? private var currentGroupCallState: PresentationGroupCallSummaryState?
private var currentIsMuted = true private var currentIsMuted = true
private var currentCantSpeak = false
private var currentMembers: PresentationGroupCallMembers? private var currentMembers: PresentationGroupCallMembers?
private var currentIsConnected = true private var currentIsConnected = true
@ -279,16 +302,24 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
strongSelf.currentMembers = members strongSelf.currentMembers = members
var isMuted = isMuted var isMuted = isMuted
var cantSpeak = false
if let state = state, let muteState = state.callState.muteState { if let state = state, let muteState = state.callState.muteState {
if !muteState.canUnmute { if !muteState.canUnmute {
isMuted = true isMuted = true
cantSpeak = true
} }
} }
if state?.callState.scheduleTimestamp != nil {
cantSpeak = true
}
strongSelf.currentIsMuted = isMuted strongSelf.currentIsMuted = isMuted
strongSelf.currentCantSpeak = cantSpeak
let currentIsConnected: Bool let currentIsConnected: Bool
if let state = state, case .connected = state.callState.networkState { if let state = state, case .connected = state.callState.networkState {
currentIsConnected = true currentIsConnected = true
} else if state?.callState.scheduleTimestamp != nil {
currentIsConnected = true
} else { } else {
currentIsConnected = false currentIsConnected = false
} }
@ -439,7 +470,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize) 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 let state: CallStatusBarBackgroundNode.State
if self.currentIsConnected {
if self.currentCantSpeak {
state = .cantSpeak
} else if self.currentIsMuted {
state = .active
} else {
state = .speaking
}
} else {
state = .connecting
}
self.backgroundNode.state = state
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0)) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
} }
} }

View File

@ -7,12 +7,29 @@ import SyncCore
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import TelegramStringFormatting
import AccountContext import AccountContext
import AppBundle import AppBundle
import SwiftSignalKit import SwiftSignalKit
import AnimatedAvatarSetNode import AnimatedAvatarSetNode
import AudioBlob import AudioBlob
func textForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(minutes):\(secondsPadding)\(seconds)"
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
let minutesPadding = minutes < 10 ? "0" : ""
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
}
}
private let titleFont = Font.semibold(15.0) private let titleFont = Font.semibold(15.0)
private let subtitleFont = Font.regular(13.0) private let subtitleFont = Font.regular(13.0)
@ -79,6 +96,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private var theme: PresentationTheme private var theme: PresentationTheme
private var strings: PresentationStrings private var strings: PresentationStrings
private var dateTimeFormat: PresentationDateTimeFormat
private let tapAction: () -> Void private let tapAction: () -> Void
@ -102,6 +120,10 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private var textIsActive = false private var textIsActive = false
private let muteIconNode: ASImageNode private let muteIconNode: ASImageNode
private var isScheduled = false
private var currentText: String = ""
private var updateTimer: SwiftSignalKit.Timer?
private let avatarsContext: AnimatedAvatarSetContext private let avatarsContext: AnimatedAvatarSetContext
private var avatarsContent: AnimatedAvatarSetContext.Content? private var avatarsContent: AnimatedAvatarSetContext.Content?
private let avatarsNode: AnimatedAvatarSetNode private let avatarsNode: AnimatedAvatarSetNode
@ -125,6 +147,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.context = context self.context = context
self.theme = presentationData.theme self.theme = presentationData.theme
self.strings = presentationData.strings self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat
self.tapAction = tapAction self.tapAction = tapAction
@ -135,6 +158,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.joinButton = HighlightableButtonNode() self.joinButton = HighlightableButtonNode()
self.joinButtonTitleNode = ImmediateTextNode() self.joinButtonTitleNode = ImmediateTextNode()
self.joinButtonBackgroundNode = ASImageNode() self.joinButtonBackgroundNode = ASImageNode()
self.joinButtonBackgroundNode.clipsToBounds = true
self.joinButtonBackgroundNode.displaysAsynchronously = false
self.joinButtonBackgroundNode.cornerRadius = 14.0
self.micButton = HighlightTrackingButtonNode() self.micButton = HighlightTrackingButtonNode()
self.micButtonForegroundNode = VoiceChatMicrophoneNode() self.micButtonForegroundNode = VoiceChatMicrophoneNode()
@ -198,6 +224,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.membersDisposable.dispose() self.membersDisposable.dispose()
self.isMutedDisposable.dispose() self.isMutedDisposable.dispose()
self.audioLevelGeneratorTimer?.invalidate() self.audioLevelGeneratorTimer?.invalidate()
self.updateTimer?.invalidate()
} }
public override func didLoad() { public override func didLoad() {
@ -250,6 +277,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
public func updatePresentationData(_ presentationData: PresentationData) { public func updatePresentationData(_ presentationData: PresentationData) {
self.theme = presentationData.theme self.theme = presentationData.theme
self.strings = presentationData.strings self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat
self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
@ -257,18 +285,31 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor
self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor) self.joinButtonTitleNode.attributedText = NSAttributedString(string: self.joinButtonTitleNode.attributedText?.string ?? "", font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor)
self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme) self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
self.updateJoinButton()
if let (size, leftInset, rightInset) = self.validLayout { if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
} }
} }
private func updateJoinButton() {
if self.isScheduled {
let purple = UIColor(rgb: 0x3252ef)
let pink = UIColor(rgb: 0xef436c)
self.joinButtonBackgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 1.0), colors: [purple, pink], locations: [0.0, 1.0], direction: .horizontal)
self.joinButtonBackgroundNode.backgroundColor = nil
} else {
self.joinButtonBackgroundNode.image = nil
self.joinButtonBackgroundNode.backgroundColor = self.theme.chat.inputPanel.actionControlFillColor
}
}
private func animateTextChange() { private func animateTextChange() {
if let snapshotView = self.textNode.view.snapshotContentTree() { if let snapshotView = self.textNode.view.snapshotContentTree() {
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0 let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
@ -298,6 +339,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else { } else {
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount)) membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
} }
self.currentText = membersText
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
@ -321,9 +363,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else { } else {
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount)) membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
} }
strongSelf.currentText = membersText
strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false) strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
if let (size, leftInset, rightInset) = strongSelf.validLayout { if let (size, leftInset, rightInset) = strongSelf.validLayout {
@ -382,7 +423,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
strongSelf.micButton.view.insertSubview(audioLevelView, at: 0) strongSelf.micButton.view.insertSubview(audioLevelView, at: 0)
} }
let level = min(1.0, max(0.0, CGFloat(value)))
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0) strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
if value > 0.0 { if value > 0.0 {
strongSelf.audioLevelView?.startAnimating() strongSelf.audioLevelView?.startAnimating()
@ -400,9 +440,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else { } else {
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount)) membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
} }
self.currentText = membersText
self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
updateAudioLevels = true updateAudioLevels = true
@ -466,6 +505,57 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)) transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
} }
var joinText = self.strings.VoiceChat_PanelJoin.uppercased()
var title = self.strings.VoiceChat_Title
var text = self.currentText
var isScheduled = false
if let scheduleTime = self.currentData?.info.scheduleTimestamp {
isScheduled = true
let timeString = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime)
if let voiceChatTitle = self.currentData?.info.title {
title = voiceChatTitle
text = self.strings.Conversation_ScheduledVoiceChatStartsOn(timeString).0
} else {
title = self.strings.Conversation_ScheduledVoiceChat
text = self.strings.Conversation_ScheduledVoiceChatStartsOnShort(timeString).0
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let elapsedTime = scheduleTime - currentTime
if elapsedTime >= 86400 {
joinText = timeIntervalString(strings: strings, value: elapsedTime)
} else if elapsedTime < 0 {
joinText = "+\(textForTimeout(value: abs(elapsedTime)))"
} else {
joinText = textForTimeout(value: elapsedTime)
}
if self.updateTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let (size, leftInset, rightInset) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.updateTimer = timer
timer.start()
}
} else {
if let timer = self.updateTimer {
self.updateTimer = nil
timer.invalidate()
}
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
title = voiceChatTitle
}
}
if self.isScheduled != isScheduled {
self.isScheduled = isScheduled
self.updateJoinButton()
}
self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.chat.inputPanel.actionControlForegroundColor)
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0) let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0)
let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize) let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize)
@ -500,15 +590,17 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.micButtonBackgroundNode.image = updatedImage self.micButtonBackgroundNode.image = updatedImage
} }
} }
var title = self.strings.VoiceChat_Title
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
title = voiceChatTitle
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width / 2.0 - 56.0, height: .greatestFiniteMagnitude)) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
var constrainedWidth = size.width / 2.0 - 56.0
if isScheduled {
constrainedWidth = size.width - 100.0
}
let titleSize = self.titleNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize)

View File

@ -624,6 +624,113 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
} }
} }
private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
let (presentationData, present, openSettings) = self.getDeviceAccessData()
let isVideo = false
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
openSettings()
}, { value in
if isVideo && value {
DeviceAccess.authorizeAccess(to: .camera(.videoCall), presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
openSettings()
}, { value in
subscriber.putNext(value)
subscriber.putCompletion()
})
} else {
subscriber.putNext(value)
subscriber.putCompletion()
}
})
return EmptyDisposable
}
|> runOn(Queue.mainQueue())
return accessEnabledSignal
|> deliverOnMainQueue
|> mapToSignal { [weak self] accessEnabled -> Signal<Bool, NoError> in
guard let strongSelf = self else {
return .single(false)
}
if !accessEnabled {
return .single(false)
}
let call = PresentationGroupCallImpl(
accountContext: accountContext,
audioSession: strongSelf.audioSession,
callKitIntegration: nil,
getDeviceAccessData: strongSelf.getDeviceAccessData,
initialCall: nil,
internalId: internalId,
peerId: peerId,
invite: nil,
joinAsPeerId: nil
)
strongSelf.updateCurrentGroupCall(call)
strongSelf.currentGroupCallPromise.set(.single(call))
strongSelf.hasActiveGroupCallsPromise.set(true)
strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak call] value in
guard let strongSelf = self, let call = call else {
return
}
if value {
if strongSelf.currentGroupCall === call {
strongSelf.updateCurrentGroupCall(nil)
strongSelf.currentGroupCallPromise.set(.single(nil))
strongSelf.hasActiveGroupCallsPromise.set(false)
}
}
}))
return .single(true)
}
}
public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult {
let begin: () -> Void = { [weak self] in
let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start()
}
if let currentGroupCall = self.currentGroupCallValue {
if endCurrentIfAny {
let endSignal = currentGroupCall.leave(terminateIfPossible: false)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue
self.startCallDisposable.set(endSignal.start(next: { _ in
begin()
}))
} else {
return .alreadyInProgress(currentGroupCall.peerId)
}
} else if let currentCall = self.currentCall {
if endCurrentIfAny {
self.callKitIntegration?.dropCall(uuid: currentCall.internalId)
self.startCallDisposable.set((currentCall.hangUp()
|> deliverOnMainQueue).start(next: { _ in
begin()
}))
} else {
return .alreadyInProgress(currentCall.peerId)
}
} else {
begin()
}
return .success
}
public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult { public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
let begin: () -> Void = { [weak self] in let begin: () -> Void = { [weak self] in
if let requestJoinAsPeerId = requestJoinAsPeerId { if let requestJoinAsPeerId = requestJoinAsPeerId {

View File

@ -77,6 +77,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
clientParams: nil, clientParams: nil,
streamDcId: nil, streamDcId: nil,
title: call.title, title: call.title,
scheduleTimestamp: call.scheduleTimestamp,
recordingStartTimestamp: nil, recordingStartTimestamp: nil,
sortAscending: true sortAscending: true
), ),
@ -120,7 +121,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
} }
return GroupCallPanelData( return GroupCallPanelData(
peerId: peerId, peerId: peerId,
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, recordingStartTimestamp: nil, sortAscending: state.sortAscending), info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, scheduleTimestamp: state.scheduleTimestamp, recordingStartTimestamp: nil, sortAscending: state.sortAscending),
topParticipants: topParticipants, topParticipants: topParticipants,
participantCount: state.totalCount, participantCount: state.totalCount,
activeSpeakers: activeSpeakers, activeSpeakers: activeSpeakers,
@ -205,7 +206,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach
} }
private extension PresentationGroupCallState { private extension PresentationGroupCallState {
static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState { static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?) -> PresentationGroupCallState {
return PresentationGroupCallState( return PresentationGroupCallState(
myPeerId: myPeerId, myPeerId: myPeerId,
networkState: .connecting, networkState: .connecting,
@ -215,7 +216,8 @@ private extension PresentationGroupCallState {
defaultParticipantMuteState: nil, defaultParticipantMuteState: nil,
recordingStartTimestamp: nil, recordingStartTimestamp: nil,
title: title, title: title,
raisedHand: false raisedHand: false,
scheduleTimestamp: scheduleTimestamp
) )
} }
} }
@ -508,6 +510,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let joinDisposable = MetaDisposable() private let joinDisposable = MetaDisposable()
private let requestDisposable = MetaDisposable() private let requestDisposable = MetaDisposable()
private let startDisposable = MetaDisposable()
private var groupCallParticipantUpdatesDisposable: Disposable? private var groupCallParticipantUpdatesDisposable: Disposable?
private let networkStateDisposable = MetaDisposable() private let networkStateDisposable = MetaDisposable()
@ -550,6 +553,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var peerUpdatesSubscription: Disposable? private var peerUpdatesSubscription: Disposable?
public private(set) var schedulePending = false
init( init(
accountContext: AccountContext, accountContext: AccountContext,
audioSession: ManagedAudioSession, audioSession: ManagedAudioSession,
@ -572,8 +577,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.peerId = peerId self.peerId = peerId
self.invite = invite self.invite = invite
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
self.schedulePending = initialCall == nil
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title) self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp)
self.statePromise = ValuePromise(self.stateValue) self.statePromise = ValuePromise(self.stateValue)
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
@ -761,7 +767,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}) })
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title)) impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title, scheduleTimestamp: initialCall.scheduleTimestamp, subscribed: initialCall.subscribed))
}) { }) {
self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId) self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId)
} else { } else {
@ -805,7 +811,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.stateValue = updatedValue strongSelf.stateValue = updatedValue
}) })
self.requestCall(movingFromBroadcastToRtc: false) if let _ = self.initialCall {
self.requestCall(movingFromBroadcastToRtc: false)
}
} }
deinit { deinit {
@ -815,6 +823,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.audioSessionDisposable?.dispose() self.audioSessionDisposable?.dispose()
self.joinDisposable.dispose() self.joinDisposable.dispose()
self.requestDisposable.dispose() self.requestDisposable.dispose()
self.startDisposable.dispose()
self.groupCallParticipantUpdatesDisposable?.dispose() self.groupCallParticipantUpdatesDisposable?.dispose()
self.leaveDisposable.dispose() self.leaveDisposable.dispose()
self.isMutedDisposable.dispose() self.isMutedDisposable.dispose()
@ -1039,287 +1048,301 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
} }
var shouldJoin = false
let activeCallInfo: GroupCallInfo?
switch previousInternalState { switch previousInternalState {
case .active: case let .active(previousCallInfo):
break if case let .active(callInfo) = internalState {
default: shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
if case let .active(callInfo) = internalState { activeCallInfo = callInfo
let callContext: OngoingGroupCallContext
if let current = self.callContext {
callContext = current
} else { } else {
var outgoingAudioBitrateKbit: Int32? activeCallInfo = nil
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 }) }
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 { default:
outgoingAudioBitrateKbit = value if case let .active(callInfo) = internalState {
} shouldJoin = callInfo.scheduleTimestamp == nil
activeCallInfo = callInfo
} else {
activeCallInfo = nil
}
}
if shouldJoin, let callInfo = activeCallInfo {
let callContext: OngoingGroupCallContext
if let current = self.callContext {
callContext = current
} else {
var outgoingAudioBitrateKbit: Int32?
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 {
outgoingAudioBitrateKbit = value
}
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
Queue.mainQueue().async { Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
}
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if case .established = strongSelf.internalState {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
self.incomingVideoSourcePromise.set(callContext.videoSources
|> deliverOnMainQueue
|> map { [weak self] sources -> [PeerId: UInt32] in
guard let strongSelf = self else { guard let strongSelf = self else {
return [:] return
} }
var result: [PeerId: UInt32] = [:] strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
for source in sources { }
if let peerId = strongSelf.ssrcMapping[source] { }, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
result[peerId] = source Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if case .established = strongSelf.internalState {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
self.incomingVideoSourcePromise.set(callContext.videoSources
|> deliverOnMainQueue
|> map { [weak self] sources -> [PeerId: UInt32] in
guard let strongSelf = self else {
return [:]
}
var result: [PeerId: UInt32] = [:]
for source in sources {
if let peerId = strongSelf.ssrcMapping[source] {
result[peerId] = source
}
}
return result
})
self.callContext = callContext
}
self.joinDisposable.set((callContext.joinPayload
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.0 != rhs.0 {
return false
}
if lhs.1 != rhs.1 {
return false
}
return true
})
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
guard let strongSelf = self else {
return
}
let peerAdminIds: Signal<[PeerId], NoError>
let peerId = strongSelf.peerId
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
peerAdminIds = Signal { subscriber in
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in
var peerIds = Set<PeerId>()
for item in list.list {
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
peerIds.insert(item.peer.id)
}
}
subscriber.putNext(Array(peerIds))
})
return disposable
}
|> distinctUntilChanged
|> runOn(.mainQueue())
} else {
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
var result: [PeerId] = []
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
if let participants = cachedData.participants {
for participant in participants.participants {
if case .creator = participant {
result.append(participant.peerId)
} else if case .admin = participant {
result.append(participant.peerId)
}
}
} }
} }
return result return result
}) }
self.callContext = callContext
} }
self.joinDisposable.set((callContext.joinPayload
|> distinctUntilChanged(isEqual: { lhs, rhs in strongSelf.currentLocalSsrc = ssrc
if lhs.0 != rhs.0 { strongSelf.requestDisposable.set((joinGroupCall(
return false account: strongSelf.account,
} peerId: strongSelf.peerId,
if lhs.1 != rhs.1 { joinAs: strongSelf.joinAsPeerId,
return false callId: callInfo.id,
} accessHash: callInfo.accessHash,
return true preferMuted: true,
}) joinPayload: joinPayload,
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in peerAdminIds: peerAdminIds,
inviteHash: strongSelf.invite
)
|> deliverOnMainQueue).start(next: { joinCallResult in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if let clientParams = joinCallResult.callInfo.clientParams {
let peerAdminIds: Signal<[PeerId], NoError> strongSelf.ssrcMapping.removeAll()
let peerId = strongSelf.peerId let addedParticipants: [(UInt32, String?)] = []
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel { for participant in joinCallResult.state.participants {
peerAdminIds = Signal { subscriber in if let ssrc = participant.ssrc {
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in strongSelf.ssrcMapping[ssrc] = participant.peer.id
var peerIds = Set<PeerId>() //addedParticipants.append((participant.ssrc, participant.jsonParams))
for item in list.list { }
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { }
peerIds.insert(item.peer.id)
} switch joinCallResult.connectionMode {
case .rtc:
strongSelf.currentConnectionMode = .rtc
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
case .broadcast:
strongSelf.currentConnectionMode = .broadcast
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
}
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
}
}, error: { error in
guard let strongSelf = self else {
return
}
if case .anonymousNotAllowed = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} else if case .tooManyParticipants = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} else if case .invalidJoinAsPeer = error {
let peerId = strongSelf.peerId
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedChannelData {
return current.withUpdatedCallJoinPeerId(nil)
} else if let current = current as? CachedGroupData {
return current.withUpdatedCallJoinPeerId(nil)
} else {
return current
} }
subscriber.putNext(Array(peerIds))
}) })
return disposable }).start()
}
|> distinctUntilChanged
|> runOn(.mainQueue())
} else {
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
var result: [PeerId] = []
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
if let participants = cachedData.participants {
for participant in participants.participants {
if case .creator = participant {
result.append(participant.peerId)
} else if case .admin = participant {
result.append(participant.peerId)
}
}
}
}
return result
}
} }
strongSelf.markAsCanBeRemoved()
strongSelf.currentLocalSsrc = ssrc
strongSelf.requestDisposable.set((joinGroupCall(
account: strongSelf.account,
peerId: strongSelf.peerId,
joinAs: strongSelf.joinAsPeerId,
callId: callInfo.id,
accessHash: callInfo.accessHash,
preferMuted: true,
joinPayload: joinPayload,
peerAdminIds: peerAdminIds,
inviteHash: strongSelf.invite
)
|> deliverOnMainQueue).start(next: { joinCallResult in
guard let strongSelf = self else {
return
}
if let clientParams = joinCallResult.callInfo.clientParams {
strongSelf.ssrcMapping.removeAll()
let addedParticipants: [(UInt32, String?)] = []
for participant in joinCallResult.state.participants {
if let ssrc = participant.ssrc {
strongSelf.ssrcMapping[ssrc] = participant.peer.id
//addedParticipants.append((participant.ssrc, participant.jsonParams))
}
}
switch joinCallResult.connectionMode {
case .rtc:
strongSelf.currentConnectionMode = .rtc
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
case .broadcast:
strongSelf.currentConnectionMode = .broadcast
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
}
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
}
}, error: { error in
guard let strongSelf = self else {
return
}
if case .anonymousNotAllowed = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} else if case .tooManyParticipants = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} else if case .invalidJoinAsPeer = error {
let peerId = strongSelf.peerId
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedChannelData {
return current.withUpdatedCallJoinPeerId(nil)
} else if let current = current as? CachedGroupData {
return current.withUpdatedCallJoinPeerId(nil)
} else {
return current
}
})
}).start()
}
strongSelf.markAsCanBeRemoved()
}))
})) }))
}))
self.networkStateDisposable.set((callContext.networkState
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
let mappedState: PresentationGroupCallState.NetworkState
if state.isConnected {
mappedState = .connected
} else {
mappedState = .connecting
}
let wasConnecting = strongSelf.stateValue.networkState == .connecting
if strongSelf.stateValue.networkState != mappedState {
strongSelf.stateValue.networkState = mappedState
}
let isConnecting = mappedState == .connecting
self.networkStateDisposable.set((callContext.networkState if strongSelf.isCurrentlyConnecting != isConnecting {
|> deliverOnMainQueue).start(next: { [weak self] state in strongSelf.isCurrentlyConnecting = isConnecting
guard let strongSelf = self else {
return
}
let mappedState: PresentationGroupCallState.NetworkState
if state.isConnected {
mappedState = .connected
} else {
mappedState = .connecting
}
let wasConnecting = strongSelf.stateValue.networkState == .connecting
if strongSelf.stateValue.networkState != mappedState {
strongSelf.stateValue.networkState = mappedState
}
let isConnecting = mappedState == .connecting
if strongSelf.isCurrentlyConnecting != isConnecting {
strongSelf.isCurrentlyConnecting = isConnecting
if isConnecting {
strongSelf.startCheckingCallIfNeeded()
} else {
strongSelf.checkCallDisposable?.dispose()
strongSelf.checkCallDisposable = nil
}
}
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
if isConnecting {
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
} else {
strongSelf.toneRenderer = nil
}
}
if isConnecting { if isConnecting {
strongSelf.didStartConnectingOnce = true strongSelf.startCheckingCallIfNeeded()
} else {
strongSelf.checkCallDisposable?.dispose()
strongSelf.checkCallDisposable = nil
} }
}
if state.isConnected {
if !strongSelf.didConnectOnce {
strongSelf.didConnectOnce = true
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
}
if let peer = strongSelf.reconnectingAsPeer { strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
strongSelf.reconnectingAsPeer = nil
strongSelf.reconnectedAsEventsPipe.putNext(peer)
}
}
}))
self.isNoiseSuppressionEnabledDisposable.set((callContext.isNoiseSuppressionEnabled
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
}))
self.audioLevelsDisposable.set((callContext.audioLevels if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
|> deliverOnMainQueue).start(next: { [weak self] levels in if isConnecting {
guard let strongSelf = self else { let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
return strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
} else {
strongSelf.toneRenderer = nil
} }
var result: [(PeerId, UInt32, Float, Bool)] = [] }
var myLevel: Float = 0.0
var myLevelHasVoice: Bool = false if isConnecting {
var missingSsrcs = Set<UInt32>() strongSelf.didStartConnectingOnce = true
for (ssrcKey, level, hasVoice) in levels { }
var peerId: PeerId?
let ssrcValue: UInt32 if state.isConnected {
switch ssrcKey { if !strongSelf.didConnectOnce {
case .local: strongSelf.didConnectOnce = true
peerId = strongSelf.joinAsPeerId
ssrcValue = 0 let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
case let .source(ssrc): strongSelf.toneRenderer = toneRenderer
peerId = strongSelf.ssrcMapping[ssrc] toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
ssrcValue = ssrc }
}
if let peerId = peerId { if let peer = strongSelf.reconnectingAsPeer {
if case .local = ssrcKey { strongSelf.reconnectingAsPeer = nil
if !strongSelf.isMutedValue.isEffectivelyMuted { strongSelf.reconnectedAsEventsPipe.putNext(peer)
myLevel = level }
myLevelHasVoice = hasVoice }
} }))
self.isNoiseSuppressionEnabledDisposable.set((callContext.isNoiseSuppressionEnabled
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
}))
self.audioLevelsDisposable.set((callContext.audioLevels
|> deliverOnMainQueue).start(next: { [weak self] levels in
guard let strongSelf = self else {
return
}
var result: [(PeerId, UInt32, Float, Bool)] = []
var myLevel: Float = 0.0
var myLevelHasVoice: Bool = false
var missingSsrcs = Set<UInt32>()
for (ssrcKey, level, hasVoice) in levels {
var peerId: PeerId?
let ssrcValue: UInt32
switch ssrcKey {
case .local:
peerId = strongSelf.joinAsPeerId
ssrcValue = 0
case let .source(ssrc):
peerId = strongSelf.ssrcMapping[ssrc]
ssrcValue = ssrc
}
if let peerId = peerId {
if case .local = ssrcKey {
if !strongSelf.isMutedValue.isEffectivelyMuted {
myLevel = level
myLevelHasVoice = hasVoice
} }
result.append((peerId, ssrcValue, level, hasVoice))
} else if ssrcValue != 0 {
missingSsrcs.insert(ssrcValue)
} }
result.append((peerId, ssrcValue, level, hasVoice))
} else if ssrcValue != 0 {
missingSsrcs.insert(ssrcValue)
} }
}
strongSelf.speakingParticipantsContext.update(levels: result)
strongSelf.speakingParticipantsContext.update(levels: result)
let mappedLevel = myLevel * 1.5
strongSelf.myAudioLevelPipe.putNext(mappedLevel) let mappedLevel = myLevel * 1.5
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) strongSelf.myAudioLevelPipe.putNext(mappedLevel)
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
if !missingSsrcs.isEmpty {
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs) if !missingSsrcs.isEmpty {
} strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
})) }
} }))
} }
switch previousInternalState { switch previousInternalState {
@ -1339,6 +1362,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if self.stateValue.title != initialState.title { if self.stateValue.title != initialState.title {
self.stateValue.title = initialState.title self.stateValue.title = initialState.title
} }
if self.stateValue.scheduleTimestamp != initialState.scheduleTimestamp {
self.stateValue.scheduleTimestamp = initialState.scheduleTimestamp
}
let accountContext = self.accountContext let accountContext = self.accountContext
let peerId = self.peerId let peerId = self.peerId
@ -1630,6 +1656,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
strongSelf.stateValue.title = state.title strongSelf.stateValue.title = state.title
strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
id: callInfo.id, id: callInfo.id,
@ -1638,6 +1665,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
clientParams: nil, clientParams: nil,
streamDcId: nil, streamDcId: nil,
title: state.title, title: state.title,
scheduleTimestamp: state.scheduleTimestamp,
recordingStartTimestamp: state.recordingStartTimestamp, recordingStartTimestamp: state.recordingStartTimestamp,
sortAscending: state.sortAscending sortAscending: state.sortAscending
)))) ))))
@ -1887,7 +1915,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> { public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
self.leaving = true self.leaving = true
if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc { if let callInfo = self.internalState.callInfo {
if terminateIfPossible { if terminateIfPossible {
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|> deliverOnMainQueue).start(completed: { [weak self] in |> deliverOnMainQueue).start(completed: { [weak self] in
@ -1896,7 +1924,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
strongSelf.markAsCanBeRemoved() strongSelf.markAsCanBeRemoved()
})) }))
} else { } else if let localSsrc = self.currentLocalSsrc {
if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl { if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl {
let account = self.account let account = self.account
let id = callInfo.id let id = callInfo.id
@ -1907,6 +1935,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
} }
self.markAsCanBeRemoved() self.markAsCanBeRemoved()
} else {
self.markAsCanBeRemoved()
} }
} else { } else {
self.markAsCanBeRemoved() self.markAsCanBeRemoved()
@ -1957,6 +1987,39 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.callContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) self.callContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
} }
public func schedule(timestamp: Int32) {
guard self.schedulePending else {
return
}
self.schedulePending = false
self.stateValue.scheduleTimestamp = timestamp
self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
guard let strongSelf = self else {
return
}
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
}))
}
public func startScheduled() {
guard case let .active(callInfo) = self.internalState else {
return
}
self.stateValue.scheduleTimestamp = nil
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
guard let strongSelf = self else {
return
}
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
}))
}
public func raiseHand() { public func raiseHand() {
guard let membersValue = self.membersValue else { guard let membersValue = self.membersValue else {
return return
@ -2207,7 +2270,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
if let value = value { if let value = value {
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title) strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: nil, subscribed: false)
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl) strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
} else { } else {
@ -2217,7 +2280,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
public func invitePeer(_ peerId: PeerId) -> Bool { public func invitePeer(_ peerId: PeerId) -> Bool {
guard case let .established(callInfo, _, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else { guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
return false return false
} }
@ -2236,11 +2299,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.invitedPeersValue = updatedInvitedPeers self.invitedPeersValue = updatedInvitedPeers
} }
public func updateTitle(_ title: String){ public func updateTitle(_ title: String) {
guard case let .established(callInfo, _, _, _, _) = self.internalState else { guard let callInfo = self.internalState.callInfo else {
return return
} }
self.stateValue.title = title
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start() let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
} }

View File

@ -27,6 +27,8 @@ private let blobSize = CGSize(width: 190.0, height: 190.0)
private let smallScale: CGFloat = 0.48 private let smallScale: CGFloat = 0.48
private let smallIconScale: CGFloat = 0.69 private let smallIconScale: CGFloat = 0.69
private let buttonHeight: CGFloat = 52.0
final class VoiceChatActionButton: HighlightTrackingButtonNode { final class VoiceChatActionButton: HighlightTrackingButtonNode {
enum State: Equatable { enum State: Equatable {
enum ActiveState: Equatable { enum ActiveState: Equatable {
@ -34,7 +36,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
case muted case muted
case on case on
} }
enum ScheduledState: Equatable {
case start
case subscribe
case unsubscribe
}
case button(text: String)
case scheduled(state: ScheduledState)
case connecting case connecting
case active(state: ActiveState) case active(state: ActiveState)
} }
@ -53,6 +63,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
private let iconNode: VoiceChatActionButtonIconNode private let iconNode: VoiceChatActionButtonIconNode
private let titleLabel: ImmediateTextNode private let titleLabel: ImmediateTextNode
private let subtitleLabel: ImmediateTextNode private let subtitleLabel: ImmediateTextNode
private let buttonTitleLabel: ImmediateTextNode
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)? private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)?
@ -103,7 +114,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
default: default:
break break
} }
case .connecting: case .connecting, .button, .scheduled:
break break
} }
} else { } else {
@ -121,12 +132,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
init() { init() {
self.bottomNode = ASDisplayNode() self.bottomNode = ASDisplayNode()
self.bottomNode.isUserInteractionEnabled = false
self.containerNode = ASDisplayNode() self.containerNode = ASDisplayNode()
self.containerNode.isUserInteractionEnabled = false
self.backgroundNode = VoiceChatActionButtonBackgroundNode() self.backgroundNode = VoiceChatActionButtonBackgroundNode()
self.iconNode = VoiceChatActionButtonIconNode(isColored: false) self.iconNode = VoiceChatActionButtonIconNode(isColored: false)
self.titleLabel = ImmediateTextNode() self.titleLabel = ImmediateTextNode()
self.subtitleLabel = ImmediateTextNode() self.subtitleLabel = ImmediateTextNode()
self.buttonTitleLabel = ImmediateTextNode()
self.buttonTitleLabel.isUserInteractionEnabled = false
self.buttonTitleLabel.alpha = 0.0
super.init() super.init()
@ -138,26 +154,38 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode) self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.buttonTitleLabel)
self.highligthedChanged = { [weak self] pressing in self.highligthedChanged = { [weak self] pressing in
if let strongSelf = self { if let strongSelf = self {
guard let (_, _, _, _, small, _, _, snap) = strongSelf.currentParams else { guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else {
return return
} }
if pressing { if pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) if case .button = state {
if small { strongSelf.containerNode.layer.removeAnimation(forKey: "opacity")
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9) strongSelf.containerNode.alpha = 0.4
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
} else { } else {
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
if small {
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9)
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
} else {
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
}
} }
} else if !strongSelf.pressing { } else if !strongSelf.pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) if case .button = state {
if small { strongSelf.containerNode.alpha = 1.0
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale) strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
} else { } else {
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
if small {
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale)
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
} else {
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
}
} }
} }
} }
@ -214,7 +242,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0 let totalHeight = titleSize.height + subtitleSize.height + 1.0
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 70.0), size: titleSize) self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 88.0), size: titleSize)
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize) self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size) self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
@ -232,7 +260,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
default: default:
break break
} }
case .connecting: case .connecting, .button, .scheduled:
break break
} }
@ -271,6 +299,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let icon: VoiceChatActionButtonIconAnimationState let icon: VoiceChatActionButtonIconAnimationState
switch state { switch state {
case .button:
icon = .empty
case let .scheduled(state):
switch state {
case .start:
icon = .start
case .subscribe:
icon = .subscribe
case .unsubscribe:
icon = .unsubscribe
}
case let .active(state): case let .active(state):
switch state { switch state {
case .on: case .on:
@ -290,7 +329,6 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.previousIcon = icon self.previousIcon = icon
self.iconNode.enqueueState(icon) self.iconNode.enqueueState(icon)
// self.iconNode.update(state: VoiceChatMicrophoneNode.State(muted: iconMuted, filled: true, color: iconColor), animated: true)
} }
func update(snap: Bool, animated: Bool) { func update(snap: Bool, animated: Bool) {
@ -312,8 +350,26 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.statePromise.set(state) self.statePromise.set(state)
if let previousState = previousState, case .button = previousState, case .scheduled = state {
self.buttonTitleLabel.alpha = 0.0
self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24)
self.iconNode.alpha = 1.0
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
}
var backgroundState: VoiceChatActionButtonBackgroundNode.State var backgroundState: VoiceChatActionButtonBackgroundNode.State
switch state { switch state {
case let .button(text):
backgroundState = .button
self.buttonTitleLabel.alpha = 1.0
self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white)
let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0))
self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize)
case .scheduled:
backgroundState = .disabled
case let .active(state): case let .active(state):
switch state { switch state {
case .on: case .on:
@ -340,14 +396,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
} }
})) }))
} else { } else {
applyParams(animated: animated) self.applyParams(animated: animated)
} }
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var hitRect = self.bounds var hitRect = self.bounds
if let (_, buttonSize, _, _, _, _, _, _) = self.currentParams { if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams {
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0) if case .button = state {
hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight)
} else {
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
}
} }
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if !hitRect.contains(point) { if !hitRect.contains(point) {
@ -453,6 +513,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
enum State: Equatable { enum State: Equatable {
case connecting case connecting
case disabled case disabled
case button
case blob(Bool) case blob(Bool)
} }
@ -546,7 +607,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskProgressLayer.lineCap = .round self.maskProgressLayer.lineCap = .round
self.maskProgressLayer.path = path self.maskProgressLayer.path = path
let largerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width + progressLineWidth, height: buttonSize.height + progressLineWidth))).cgPath let circleFrame = CGRect(origin: CGPoint(x: (358 - buttonSize.width) / 2.0, y: (358 - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
self.maskCircleLayer.fillColor = white.cgColor self.maskCircleLayer.fillColor = white.cgColor
self.maskCircleLayer.path = largerCirclePath self.maskCircleLayer.path = largerCirclePath
self.maskCircleLayer.isHidden = true self.maskCircleLayer.isHidden = true
@ -825,7 +888,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskBlobView.startAnimating() self.maskBlobView.startAnimating()
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
} }
private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) { private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) {
CATransaction.begin() CATransaction.begin()
let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0)
@ -872,7 +935,8 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.updateGlowAndGradientAnimations(type: type, previousType: nil) self.updateGlowAndGradientAnimations(type: type, previousType: nil)
if case .blob = self.state { if case .connecting = self.state {
} else {
self.maskBlobView.isHidden = false self.maskBlobView.isHidden = false
self.maskBlobView.startAnimating() self.maskBlobView.startAnimating()
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
@ -907,6 +971,47 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
CATransaction.commit() CATransaction.commit()
} }
private func setupButtonAnimation() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.backgroundCircleLayer.isHidden = true
self.foregroundCircleLayer.isHidden = true
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = true
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight), cornerRadius: 10.0).cgPath
self.maskCircleLayer.path = path
CATransaction.commit()
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil)
self.updatedActive?(true)
}
private func playScheduledAnimation() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.maskGradientLayer.isHidden = false
CATransaction.commit()
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
let previousPath = self.maskCircleLayer.path
self.maskCircleLayer.path = largerCirclePath
self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.42, initialVelocity: 0.0, damping: 104.0)
self.maskBlobView.isHidden = false
self.maskBlobView.startAnimating()
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? 0.8)
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
var isActive = false var isActive = false
func updateAnimations() { func updateAnimations() {
if !self.isCurrentlyInHierarchy { if !self.isCurrentlyInHierarchy {
@ -959,7 +1064,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.isActive = false self.isActive = false
if let transition = self.transition { if let transition = self.transition {
if case .connecting = transition { if case .button = transition {
self.playScheduledAnimation()
} else if case .connecting = transition {
self.playConnectionAnimation(type: .muted) { [weak self] in self.playConnectionAnimation(type: .muted) { [weak self] in
self?.isActive = false self?.isActive = false
} }
@ -969,7 +1076,10 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
} }
self.transition = nil self.transition = nil
} }
break case .button:
self.updatedActive?(true)
self.isActive = false
self.setupButtonAnimation()
} }
} }
@ -1037,20 +1147,24 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
override func layout() { override func layout() {
super.layout() super.layout()
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height)
let center = bounds.center
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize) self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize)
let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize)
self.backgroundCircleLayer.frame = circleFrame self.backgroundCircleLayer.frame = circleFrame
self.foregroundCircleLayer.position = center self.foregroundCircleLayer.position = center
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth)) self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
self.growingForegroundCircleLayer.position = center self.growingForegroundCircleLayer.position = center
self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds
self.maskCircleLayer.frame = circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) self.maskCircleLayer.frame = self.bounds
// circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0) self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0)
self.foregroundView.frame = self.bounds self.foregroundView.frame = self.bounds
self.foregroundGradientLayer.frame = self.bounds self.foregroundGradientLayer.frame = self.bounds
self.maskGradientLayer.position = center self.maskGradientLayer.position = center
self.maskGradientLayer.bounds = self.bounds self.maskGradientLayer.bounds = bounds
self.maskView.frame = self.bounds self.maskView.frame = self.bounds
} }
} }
@ -1386,6 +1500,10 @@ final class BlobView: UIView {
} }
enum VoiceChatActionButtonIconAnimationState: Equatable { enum VoiceChatActionButtonIconAnimationState: Equatable {
case empty
case start
case subscribe
case unsubscribe
case unmute case unmute
case mute case mute
case hand case hand
@ -1399,6 +1517,7 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
self.isColored = isColored self.isColored = isColored
super.init(size: CGSize(width: 100.0, height: 100.0)) super.init(size: CGSize(width: 100.0, height: 100.0))
self.scale = 0.8
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1)) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
} }
@ -1410,30 +1529,73 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
let previousState = self.iconState let previousState = self.iconState
self.iconState = state self.iconState = state
if state != .empty {
self.alpha = 1.0
}
switch previousState { switch previousState {
case .empty:
switch state {
case .start:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
default:
break
}
case .subscribe:
switch state {
case .unsubscribe:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
case .mute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
case .hand:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
default:
break
}
case .unsubscribe:
switch state {
case .subscribe:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
case .mute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
case .hand:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
default:
break
}
case .start:
switch state {
case .mute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
default:
break
}
case .unmute: case .unmute:
switch state { switch state {
case .mute: case .mute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute"))) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute")))
case .hand: case .hand:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2"))) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2")))
case .unmute: default:
break break
} }
case .mute: case .mute:
switch state { switch state {
case .start:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
case .unmute: case .unmute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 12), duration: 0.2)) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute")))
case .hand: case .hand:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff"))) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff")))
case .mute: case .empty:
self.alpha = 0.0
default:
break break
} }
case .hand: case .hand:
switch state { switch state {
case .mute, .unmute: case .mute, .unmute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn"))) self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn")))
case .hand: default:
break break
} }
} }

View File

@ -5,6 +5,7 @@ import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import TelegramStringFormatting
import TelegramVoip import TelegramVoip
import TelegramAudio import TelegramAudio
import AccountContext import AccountContext
@ -29,6 +30,7 @@ import LegacyComponents
import LegacyMediaPickerUI import LegacyMediaPickerUI
import WebSearchUI import WebSearchUI
import MapResourceToAvatarSizes import MapResourceToAvatarSizes
import SolidRoundedButtonNode
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) 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) })?.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 { final class GroupVideoNode: ASDisplayNode {
private let videoViewContainer: UIView private let videoViewContainer: UIView
private let videoView: PresentationCallVideoView private let videoView: PresentationCallVideoView
@ -730,7 +633,15 @@ public final class VoiceChatController: ViewController {
private let leftBorderNode: ASDisplayNode private let leftBorderNode: ASDisplayNode
private let rightBorderNode: 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 enqueuedTransitions: [ListTransition] = []
private var floatingHeaderOffset: CGFloat? private var floatingHeaderOffset: CGFloat?
@ -823,6 +734,8 @@ public final class VoiceChatController: ViewController {
self.context = call.accountContext self.context = call.accountContext
self.call = call self.call = call
self.isScheduling = call.schedulePending
let presentationData = sharedContext.currentPresentationData.with { $0 } let presentationData = sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData self.presentationData = presentationData
@ -836,7 +749,7 @@ public final class VoiceChatController: ViewController {
self.contentContainer.isHidden = true self.contentContainer.isHidden = true
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor
self.backgroundNode.clipsToBounds = false self.backgroundNode.clipsToBounds = false
if sharedContext.immediateExperimentalUISettings.demoVideoChats { if sharedContext.immediateExperimentalUISettings.demoVideoChats {
@ -844,6 +757,8 @@ public final class VoiceChatController: ViewController {
} }
self.listNode = ListView() 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.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
self.listNode.clipsToBounds = true self.listNode.clipsToBounds = true
self.listNode.scroller.bounces = false self.listNode.scroller.bounces = false
@ -870,7 +785,7 @@ public final class VoiceChatController: ViewController {
self.closeButton = VoiceChatHeaderButton(context: self.context) self.closeButton = VoiceChatHeaderButton(context: self.context)
self.closeButton.setContent(.image(closeButtonImage(dark: false))) 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 = ASImageNode()
self.topCornersNode.displaysAsynchronously = false self.topCornersNode.displaysAsynchronously = false
@ -895,6 +810,13 @@ public final class VoiceChatController: ViewController {
self.switchCameraButton.isUserInteractionEnabled = false self.switchCameraButton.isUserInteractionEnabled = false
self.leaveButton = CallControllerButtonItemNode() self.leaveButton = CallControllerButtonItemNode()
self.actionButton = VoiceChatActionButton() 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 = ASDisplayNode()
self.leftBorderNode.backgroundColor = panelBackgroundColor self.leftBorderNode.backgroundColor = panelBackgroundColor
@ -906,6 +828,19 @@ public final class VoiceChatController: ViewController {
self.rightBorderNode.isUserInteractionEnabled = false self.rightBorderNode.isUserInteractionEnabled = false
self.rightBorderNode.clipsToBounds = 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() super.init()
let statePromise = ValuePromise(State(), ignoreRepeated: true) 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) 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) strongSelf.controller?.presentInGlobalOverlay(contextController)
}, setPeerIdWithRevealedOptions: { peerId, _ in }, setPeerIdWithRevealedOptions: { peerId, _ in
updateState { state in updateState { state in
@ -1550,6 +1486,7 @@ public final class VoiceChatController: ViewController {
} }
self.bottomPanelNode.addSubnode(self.leaveButton) self.bottomPanelNode.addSubnode(self.leaveButton)
self.bottomPanelNode.addSubnode(self.actionButton) self.bottomPanelNode.addSubnode(self.actionButton)
self.bottomPanelNode.addSubnode(self.scheduleCancelButton)
self.addSubnode(self.dimNode) self.addSubnode(self.dimNode)
self.addSubnode(self.contentContainer) self.addSubnode(self.contentContainer)
@ -1563,6 +1500,7 @@ public final class VoiceChatController: ViewController {
self.contentContainer.addSubnode(self.leftBorderNode) self.contentContainer.addSubnode(self.leftBorderNode)
self.contentContainer.addSubnode(self.rightBorderNode) self.contentContainer.addSubnode(self.rightBorderNode)
self.contentContainer.addSubnode(self.bottomPanelNode) self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode)
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in |> 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))) let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
strongSelf.currentSubtitle = subtitle 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.optionsButtonIsAvatar = false
strongSelf.optionsButton.isUserInteractionEnabled = true strongSelf.optionsButton.isUserInteractionEnabled = true
strongSelf.optionsButton.alpha = 1.0 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 self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1874,22 +1808,32 @@ public final class VoiceChatController: ViewController {
})) }))
self.titleNode.tapped = { [weak self] in self.titleNode.tapped = { [weak self] in
if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden { if let strongSelf = self {
var hasTooltipAlready = false if strongSelf.callState?.canManageCall ?? false {
strongSelf.controller?.forEachController { controller -> Bool in strongSelf.openTitleEditing()
if controller is TooltipScreen { } else if !strongSelf.titleNode.recordingIconNode.isHidden {
hasTooltipAlready = true 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 { deinit {
@ -1931,7 +1875,7 @@ public final class VoiceChatController: ViewController {
let avatarSize = CGSize(width: 28.0, height: 28.0) 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) |> take(1)
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in |> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
@ -1965,15 +1909,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.openTitleEditing()
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))
}))) })))
var hasPermissions = true var hasPermissions = true
@ -1994,16 +1930,7 @@ public final class VoiceChatController: ViewController {
c.setItems(strongSelf.contextMenuPermissionItems()) 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 { if let inviteLinks = inviteLinks {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in 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) 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)) self?.controller?.present(alertController, in: .window(.root))
}), false)) }), false))
} else { } else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in if strongSelf.callState?.scheduleTimestamp == nil {
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
}, action: { _, f in return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
f(.dismissWithoutContent) }, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else { guard let strongSelf = self else {
return 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)
} }
})
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 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.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer) 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() { @objc private func optionsPressed() {
@ -2491,7 +2575,31 @@ public final class VoiceChatController: ViewController {
guard let callState = self.callState else { guard let callState = self.callState else {
return 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 return
} }
if let muteState = callState.muteState { if let muteState = callState.muteState {
@ -2548,11 +2656,27 @@ public final class VoiceChatController: ViewController {
} }
@objc private func actionButtonPressed() { @objc private func actionButtonPressed() {
if self.isScheduling {
self.schedule()
}
} }
@objc private func audioOutputPressed() { @objc private func audioOutputPressed() {
self.hapticFeedback.impact(.light) 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 { guard let (availableOutputs, currentOutput) = self.audioOutputState else {
return return
} }
@ -2743,8 +2867,8 @@ public final class VoiceChatController: ViewController {
} }
var isFullscreen = false var isFullscreen = false
func updateIsFullscreen(_ isFullscreen: Bool) { func updateIsFullscreen(_ isFullscreen: Bool, force: Bool = false) {
guard self.isFullscreen != isFullscreen, let (layout, _) = self.validLayout else { guard self.isFullscreen != isFullscreen || force, let (layout, _) = self.validLayout else {
return return
} }
self.isFullscreen = isFullscreen 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) 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) let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0) 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.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.topPanelEdgeNode, 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.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.leftBorderNode, 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)
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
if let snapshotView = self.topCornersNode.view.snapshotContentTree() { if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
snapshotView.frame = self.topCornersNode.frame snapshotView.frame = self.topCornersNode.frame
@ -2814,22 +2942,39 @@ public final class VoiceChatController: ViewController {
return return
} }
var title = self.currentTitle 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 { if let navigationController = self.controller?.navigationController as? NavigationController {
for controller in navigationController.viewControllers.reversed() { for controller in navigationController.viewControllers.reversed() {
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId { 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 var size = layout.size
if case .regular = layout.metrics.widthClass { if case .regular = layout.metrics.widthClass {
size.width = floor(min(size.width, size.height) * 0.5) 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) { private func updateButtons(animated: Bool) {
@ -2866,7 +3011,7 @@ public final class VoiceChatController: ViewController {
coloredButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) 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 soundAppearance: CallControllerButtonItemNode.Content.Appearance = coloredButtonAppearance
var soundTitle: String = self.presentationData.strings.Call_Speaker var soundTitle: String = self.presentationData.strings.Call_Speaker
switch audioMode { switch audioMode {
@ -2890,6 +3035,12 @@ public final class VoiceChatController: ViewController {
soundTitle = self.presentationData.strings.Call_Audio 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 let videoButtonSize: CGSize
var buttonsTitleAlpha: CGFloat var buttonsTitleAlpha: CGFloat
switch self.displayMode { switch self.displayMode {
@ -2916,6 +3067,7 @@ public final class VoiceChatController: ViewController {
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha) transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
} }
private var ignoreNextConnecting = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstTime = self.validLayout == nil let isFirstTime = self.validLayout == nil
self.validLayout = (layout, navigationHeight) 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)) 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) 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 cameraButtonSize = CGSize(width: 36.0, height: 36.0)
let sideButtonMinimalInset: CGFloat = 16.0 let sideButtonMinimalInset: CGFloat = 16.0
let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.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 actionButtonTitle: String
let actionButtonSubtitle: String let actionButtonSubtitle: String
var actionButtonEnabled = true var actionButtonEnabled = true
if let callState = self.callState { if let callState = self.callState, !self.isScheduling {
switch callState.networkState { var isScheduled = callState.scheduleTimestamp != nil
case .connecting: 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 actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = "" actionButtonSubtitle = ""
actionButtonEnabled = false 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.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) 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 { if self.actionButton.supernode === self.bottomPanelNode {
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame) transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame)
} }
@ -3196,6 +3385,12 @@ public final class VoiceChatController: ViewController {
} }
self.enqueuedTransitions.remove(at: 0) 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() var options = ListViewDeleteAndInsertOptions()
let isFirstTime = self.isFirstTime let isFirstTime = self.isFirstTime
if isFirstTime { if isFirstTime {
@ -3235,7 +3430,11 @@ public final class VoiceChatController: ViewController {
let listTopInset = layoutTopInset + 63.0 let listTopInset = layoutTopInset + 63.0
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) 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) let targetY = listTopInset + (self.topInset ?? listSize.height)
@ -3453,9 +3652,12 @@ public final class VoiceChatController: ViewController {
} }
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 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) 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 return false
} }
} }
@ -3494,6 +3696,9 @@ public final class VoiceChatController: ViewController {
self.controller?.dismissAllTooltips() self.controller?.dismissAllTooltips()
case .changed: case .changed:
var translation = recognizer.translation(in: self.contentContainer.view).y 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 var topInset: CGFloat = 0.0
if let (currentTopInset, currentPanOffset) = self.panGestureArguments { if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
topInset = currentTopInset topInset = currentTopInset
@ -3591,9 +3796,13 @@ public final class VoiceChatController: ViewController {
self.panGestureArguments = nil self.panGestureArguments = nil
var dismissing = false var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { 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 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 { if velocity.y > -1500.0 && !self.isFullscreen {
DispatchQueue.main.async { 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 }) 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.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
self.animatingExpansion = false self.animatingExpansion = false
}) })
} else { } else if !self.isScheduling {
self.updateIsFullscreen(false) self.updateIsFullscreen(false)
self.animatingExpansion = true self.animatingExpansion = true
self.listNode.scroller.setContentOffset(CGPoint(), animated: false) 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 = {}) { private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let peerId = self.callState?.myPeerId else { guard let peerId = self.callState?.myPeerId else {
return return
@ -3765,7 +3992,7 @@ public final class VoiceChatController: ViewController {
return return
} }
let proceed = { let proceed = {
let _ = strongSelf.currentAvatarMixin.swap(nil) let _ = strongSelf.currentAvatarMixin.swap(nil)
let postbox = strongSelf.context.account.postbox 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 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 let count = navigationController.viewControllers.count
if count == 2 || navigationController.viewControllers[count - 2] is ChatController { if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue { 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 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 if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
} else { } else {

View File

@ -145,7 +145,7 @@ public final class VoiceChatJoinScreen: ViewController {
defaultJoinAsPeerId = cachedData.callJoinPeerId defaultJoinAsPeerId = cachedData.callJoinPeerId
} }
let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title) let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title, scheduleTimestamp: call.info.scheduleTimestamp, subscribed: false)
if availablePeers.count > 0 && defaultJoinAsPeerId == nil { if availablePeers.count > 0 && defaultJoinAsPeerId == nil {
strongSelf.dismiss() strongSelf.dismiss()
strongSelf.join(activeCall) strongSelf.join(activeCall)

View File

@ -396,7 +396,7 @@ public final class VoiceChatOverlayController: ViewController {
var slide = true var slide = true
var hidden = true var hidden = true
var animated = true var animated = true
var animateInsets = true
if controllers.count == 1 || controllers.last is ChatController { if controllers.count == 1 || controllers.last is ChatController {
if let chatController = controllers.last as? ChatController { if let chatController = controllers.last as? ChatController {
slide = false slide = false
@ -416,9 +416,13 @@ public final class VoiceChatOverlayController: ViewController {
hidden = true hidden = true
} }
if case .active(.cantSpeak) = state { switch state {
hidden = true case .active(.cantSpeak), .button, .scheduled:
hidden = true
default:
break
} }
if hasVoiceChatController { if hasVoiceChatController {
hidden = false hidden = false
animated = self.initiallyHidden animated = self.initiallyHidden
@ -429,7 +433,6 @@ public final class VoiceChatOverlayController: ViewController {
let previousInsets = self.additionalSideInsets let previousInsets = self.additionalSideInsets
self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
if previousInsets != self.additionalSideInsets { if previousInsets != self.additionalSideInsets {
self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
} }

View File

@ -0,0 +1,143 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import TelegramStringFormatting
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
final class VoiceChatTimerNode: ASDisplayNode {
private let strings: PresentationStrings
private let dateTimeFormat: PresentationDateTimeFormat
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let timerNode: ImmediateTextNode
private let foregroundView = UIView()
private let foregroundGradientLayer = CAGradientLayer()
private let maskView = UIView()
private var validLayout: CGSize?
private var updateTimer: SwiftSignalKit.Timer?
init(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) {
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.titleNode = ImmediateTextNode()
self.subtitleNode = ImmediateTextNode()
self.timerNode = ImmediateTextNode()
super.init()
self.allowsGroupOpacity = true
self.foregroundGradientLayer.type = .radial
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.foregroundView.mask = self.maskView
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
self.view.addSubview(self.foregroundView)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.maskView.addSubnode(self.timerNode)
}
deinit {
self.updateTimer?.invalidate()
}
func animateIn() {
self.foregroundView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
}
private func setupGradientAnimations() {
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.foregroundGradientLayer.startPoint
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45))
self.foregroundGradientLayer.startPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "startPoint")
animation.duration = Double.random(in: 0.8 ..< 1.4)
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
self?.setupGradientAnimations()
// }
}
self.foregroundGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
}
func update(size: CGSize, scheduleTime: Int32?, transition: ContainedViewLayoutTransition) {
if self.validLayout == nil {
self.setupGradientAnimations()
}
self.validLayout = size
guard let scheduleTime = scheduleTime else {
return
}
self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
self.foregroundGradientLayer.frame = self.foregroundView.bounds
self.maskView.frame = self.foregroundView.bounds
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let elapsedTime = scheduleTime - currentTime
let timerText: String
if elapsedTime >= 86400 {
timerText = timeIntervalString(strings: self.strings, value: elapsedTime)
} else if elapsedTime < 0 {
timerText = "\(textForTimeout(value: abs(elapsedTime)))"
} else {
timerText = textForTimeout(value: elapsedTime)
}
if self.updateTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let size = strongSelf.validLayout {
strongSelf.update(size: size, scheduleTime: scheduleTime, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.updateTimer = timer
timer.start()
}
let subtitle = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime)
self.titleNode.attributedText = NSAttributedString(string: elapsedTime < 0 ? self.strings.VoiceChat_LateBy : self.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
let titleSize = self.titleNode.updateLayout(size)
self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
self.timerNode.attributedText = NSAttributedString(string: timerText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
let timerSize = self.timerNode.updateLayout(size)
self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 80.0, width: timerSize.width, height: timerSize.height)
self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 21.0, design: .round, weight: .semibold, traits: []), textColor: .white)
let subtitleSize = self.subtitleNode.updateLayout(size)
self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: timerSize.width, height: subtitleSize.height)
self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
}
}

View File

@ -47,7 +47,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
private let maxLength: Int private let maxLength: Int
init(theme: PresentationTheme, placeholder: String, maxLength: Int) { init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
self.theme = theme self.theme = theme
self.maxLength = maxLength self.maxLength = maxLength
@ -65,7 +65,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = .default self.textInputNode.keyboardType = .default
self.textInputNode.autocapitalizationType = .sentences self.textInputNode.autocapitalizationType = .sentences
self.textInputNode.returnKeyType = .done self.textInputNode.returnKeyType = returnKeyType
self.textInputNode.autocorrectionType = .default self.textInputNode.autocorrectionType = .default
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
@ -510,7 +510,7 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
self.titleNode = ASTextNode() self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2 self.titleNode.maximumNumberOfLines = 2
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength) self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next)
self.firstNameInputFieldNode.text = firstNameValue ?? "" self.firstNameInputFieldNode.text = firstNameValue ?? ""
self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength) self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
@ -550,14 +550,6 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
self.addSubnode(separatorNode) self.addSubnode(separatorNode)
} }
self.firstNameInputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.validLayout {
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
}
}
}
self.updateTheme(theme) self.updateTheme(theme)
} }

View File

@ -0,0 +1,103 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class VoiceChatTitleNode: ASDisplayNode {
private var theme: PresentationTheme
private let titleNode: ASTextNode
private let infoNode: ASTextNode
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)
}
}

View File

@ -259,9 +259,9 @@ public func startScheduledGroupCall(account: Account, peerId: PeerId, callId: In
return account.postbox.transaction { transaction -> GroupCallInfo in return account.postbox.transaction { transaction -> GroupCallInfo in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData { if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false)) return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
} else if let cachedData = cachedData as? CachedGroupData { } else if let cachedData = cachedData as? CachedGroupData {
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false)) return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
} else { } else {
return cachedData return cachedData
} }
@ -331,15 +331,27 @@ public func updateGroupCallJoinAsPeer(account: Account, peerId: PeerId, joinAs:
} }
|> castError(UpdateGroupCallJoinAsPeerError.self) |> castError(UpdateGroupCallJoinAsPeerError.self)
|> mapToSignal { result in |> mapToSignal { result in
guard let (peer, joinAs) = result else { guard let (inputPeer, joinInputPeer) = result else {
return .fail(.generic) return .fail(.generic)
} }
return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: peer, joinAs: joinAs)) return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: inputPeer, joinAs: joinInputPeer))
|> mapError { _ -> UpdateGroupCallJoinAsPeerError in |> mapError { _ -> UpdateGroupCallJoinAsPeerError in
return .generic return .generic
} }
|> mapToSignal { result -> Signal<Never, UpdateGroupCallJoinAsPeerError> in |> mapToSignal { result -> Signal<Never, UpdateGroupCallJoinAsPeerError> in
return .complete() return account.postbox.transaction { transaction in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedCallJoinPeerId(joinAs)
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.withUpdatedCallJoinPeerId(joinAs)
} else {
return cachedData
}
})
}
|> castError(UpdateGroupCallJoinAsPeerError.self)
|> ignoreValues
} }
} }
} }
@ -644,9 +656,9 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
return account.postbox.transaction { transaction -> JoinGroupCallResult in return account.postbox.transaction { transaction -> JoinGroupCallResult in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData { if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedCallJoinPeerId(joinAs) return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
} else if let cachedData = cachedData as? CachedGroupData { } else if let cachedData = cachedData as? CachedGroupData {
return cachedData.withUpdatedCallJoinPeerId(joinAs) return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
} else { } else {
return cachedData return cachedData
} }

View File

@ -85,7 +85,7 @@ public enum MessageContentKind: Equatable {
} }
} }
public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind { public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> MessageContentKind {
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute { if let attribute = attribute as? RestrictedContentMessageAttribute {
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) { if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) {
@ -95,14 +95,14 @@ public func messageContentKind(contentSettings: ContentSettings, message: Messag
} }
} }
for media in message.media { for media in message.media {
if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) { if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) {
return kind return kind
} }
} }
return .text(message.text) return .text(message.text)
} }
public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? { public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? {
switch media { switch media {
case let expiredMedia as TelegramMediaExpiredContent: case let expiredMedia as TelegramMediaExpiredContent:
switch expiredMedia.data { switch expiredMedia.data {
@ -163,7 +163,7 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P
} }
case _ as TelegramMediaAction: case _ as TelegramMediaAction:
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId { if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) ?? "") return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
} else { } else {
return nil return nil
} }
@ -223,8 +223,8 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation
} }
} }
public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> (String, Bool) {
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
return (foldLineBreaks(message.text), false) return (foldLineBreaks(message.text), false)
} }

View File

@ -27,11 +27,11 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P
return result return result
} }
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? { public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
} }
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? { public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? {
var attributedString: NSAttributedString? var attributedString: NSAttributedString?
let primaryTextColor: UIColor let primaryTextColor: UIColor
@ -448,7 +448,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
case let .groupPhoneCall(_, _, scheduleDate, duration): case let .groupPhoneCall(_, _, scheduleDate, duration):
if let scheduleDate = scheduleDate { if let scheduleDate = scheduleDate {
let titleString = strings.Notification_VoiceChatScheduled let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
let titleString = strings.Notification_VoiceChatScheduled(timeString).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
} else if let duration = duration { } else if let duration = duration {
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -302,6 +302,10 @@ public final class AccountContextImpl: AccountContext {
} }
} }
public func scheduleGroupCall(peerId: PeerId) {
let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true)
}
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) { public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) {
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false) let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {

View File

@ -356,7 +356,7 @@ final class AuthorizedApplicationContext {
if inAppNotificationSettings.displayPreviews { if inAppNotificationSettings.displayPreviews {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: { strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: {
if let strongSelf = self { if let strongSelf = self {
var foundOverlay = false var foundOverlay = false
strongSelf.mainWindow.forEachViewController({ controller in strongSelf.mainWindow.forEachViewController({ controller in

View File

@ -177,14 +177,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
controller.inProgress = false controller.inProgress = false
let text: String let text: String
var actions: [TextAlertAction] = [ var actions: [TextAlertAction] = []
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]
switch error { switch error {
case .limitExceeded: case .limitExceeded:
text = strongSelf.presentationData.strings.Login_CodeFloodError text = strongSelf.presentationData.strings.Login_CodeFloodError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .invalidPhoneNumber: case .invalidPhoneNumber:
text = strongSelf.presentationData.strings.Login_InvalidPhoneError text = strongSelf.presentationData.strings.Login_InvalidPhoneError
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
guard let strongSelf = self, let controller = controller else { guard let strongSelf = self, let controller = controller else {
return return
@ -200,8 +200,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
})) }))
case .phoneLimitExceeded: case .phoneLimitExceeded:
text = strongSelf.presentationData.strings.Login_PhoneFloodError text = strongSelf.presentationData.strings.Login_PhoneFloodError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .phoneBanned: case .phoneBanned:
text = strongSelf.presentationData.strings.Login_PhoneBannedError text = strongSelf.presentationData.strings.Login_PhoneBannedError
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
guard let strongSelf = self, let controller = controller else { guard let strongSelf = self, let controller = controller else {
return return
@ -217,6 +219,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
})) }))
case let .generic(info): case let .generic(info):
text = strongSelf.presentationData.strings.Login_UnknownError text = strongSelf.presentationData.strings.Login_UnknownError
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
guard let strongSelf = self, let controller = controller else { guard let strongSelf = self, let controller = controller else {
return return
@ -238,6 +241,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
})) }))
case .timeout: case .timeout:
text = strongSelf.presentationData.strings.Login_NetworkError text = strongSelf.presentationData.strings.Login_NetworkError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in
guard let strongSelf = self, let controller = controller else { guard let strongSelf = self, let controller = controller else {
return return

View File

@ -535,7 +535,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
case .groupPhoneCall, .inviteToGroupPhoneCall: case .groupPhoneCall, .inviteToGroupPhoneCall:
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall { if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title)) strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribed: activeCall.subscribed))
} else { } else {
var canManageGroupCalls = false var canManageGroupCalls = false
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel { if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
@ -564,12 +564,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
statusController?.dismiss() statusController?.dismiss()
} }
strongSelf.present(statusController, in: .window(.root)) strongSelf.present(statusController, in: .window(.root))
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId) strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId, title: nil, scheduleDate: nil)
|> deliverOnMainQueue).start(next: { [weak self] info in |> deliverOnMainQueue).start(next: { [weak self] info in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribed: false))
}, error: { [weak self] error in }, error: { [weak self] error in
dismissStatus?() dismissStatus?()

View File

@ -32,7 +32,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return editPanelNode return editPanelNode
} else { } else {
let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder) let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
panelNode.interfaceInteraction = interfaceInteraction panelNode.interfaceInteraction = interfaceInteraction
return panelNode return panelNode
} }
@ -63,7 +63,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return replyPanelNode return replyPanelNode
} else { } else {
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder) let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
panelNode.interfaceInteraction = interfaceInteraction panelNode.interfaceInteraction = interfaceInteraction
return panelNode return panelNode
} }

View File

@ -18,8 +18,8 @@ import UniversalMediaPlayer
import TelegramUniversalVideoContent import TelegramUniversalVideoContent
import GalleryUI import GalleryUI
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? { private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false)
} }
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
@ -132,7 +132,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, accountPeerId: item.context.account.peerId) let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId)
var image: TelegramMediaImage? var image: TelegramMediaImage?
for media in item.message.media { for media in item.message.media {

View File

@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData {
if let chatPeer = message.peers[item.message.id.peerId] { if let chatPeer = message.peers[item.message.id.peerId] {
let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId) let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
var text = messageText var text = messageText

View File

@ -18,6 +18,7 @@ import TelegramStringFormatting
public final class ChatMessageNotificationItem: NotificationItem { public final class ChatMessageNotificationItem: NotificationItem {
let context: AccountContext let context: AccountContext
let strings: PresentationStrings let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder let nameDisplayOrder: PresentationPersonNameOrder
let messages: [Message] let messages: [Message]
let tapAction: () -> Bool let tapAction: () -> Bool
@ -27,9 +28,10 @@ public final class ChatMessageNotificationItem: NotificationItem {
return messages.first?.id.peerId return messages.first?.id.peerId
} }
public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) { public init(context: AccountContext, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) {
self.context = context self.context = context
self.strings = strings self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.messages = messages self.messages = messages
self.tapAction = tapAction self.tapAction = tapAction
@ -181,7 +183,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
if message.containsSecretMedia { if message.containsSecretMedia {
imageDimensions = nil imageDimensions = nil
} }
messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, accountPeerId: item.context.account.peerId).0 messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId).0
} else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] { } else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] {
var displayAuthor = true var displayAuthor = true
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
@ -218,9 +220,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
} }
} }
} else if item.messages[0].groupingKey != nil { } else if item.messages[0].groupingKey != nil {
var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId).key var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId).key
for i in 1 ..< item.messages.count { for i in 1 ..< item.messages.count {
let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
if kind != nextKind.key { if kind != nextKind.key {
kind = .text kind = .text
break break

View File

@ -65,7 +65,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
} }
} }
let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: context.account.peerId) let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId)
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
let titleColor: UIColor let titleColor: UIColor

View File

@ -269,7 +269,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.currentMessage = interfaceState.pinnedMessage self.currentMessage = interfaceState.pinnedMessage
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
} }
} }
@ -314,14 +314,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.currentLayout = (width, leftInset, rightInset) self.currentLayout = (width, leftInset, rightInset)
if let currentMessage = self.currentMessage { if let currentMessage = self.currentMessage {
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
} }
} }
return panelHeight return panelHeight
} }
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) { private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
let message = pinnedMessage.message let message = pinnedMessage.message
var animationTransition: ContainedViewLayoutTransition = .immediate var animationTransition: ContainedViewLayoutTransition = .immediate
@ -470,7 +470,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
} }
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings) let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
Queue.mainQueue().async { Queue.mainQueue().async {
if let strongSelf = self { if let strongSelf = self {

View File

@ -262,12 +262,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
} }
} }
private let calendar = Calendar(identifier: .gregorian)
private func updateButtonTitle() { private func updateButtonTitle() {
guard let date = self.pickerView?.date else { guard let date = self.pickerView?.date else {
return return
} }
let calendar = Calendar(identifier: .gregorian)
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat) let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
switch mode { switch mode {
case .scheduledMessages: case .scheduledMessages:

View File

@ -15,6 +15,7 @@ import PhotoResources
import TelegramStringFormatting import TelegramStringFormatting
final class EditAccessoryPanelNode: AccessoryPanelNode { final class EditAccessoryPanelNode: AccessoryPanelNode {
let dateTimeFormat: PresentationDateTimeFormat
let messageId: MessageId let messageId: MessageId
let closeButton: ASButtonNode let closeButton: ASButtonNode
@ -67,12 +68,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
var strings: PresentationStrings var strings: PresentationStrings
var nameDisplayOrder: PresentationPersonNameOrder var nameDisplayOrder: PresentationPersonNameOrder
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
self.context = context self.context = context
self.messageId = messageId self.messageId = messageId
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
self.closeButton = ASButtonNode() self.closeButton = ASButtonNode()
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
@ -159,7 +161,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
if let currentEditMediaReference = self.currentEditMediaReference { if let currentEditMediaReference = self.currentEditMediaReference {
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
} }
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, accountPeerId: self.context.account.peerId) (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId)
} }
var updatedMediaReference: AnyMediaReference? var updatedMediaReference: AnyMediaReference?
@ -231,7 +233,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
if let currentEditMediaReference = self.currentEditMediaReference { if let currentEditMediaReference = self.currentEditMediaReference {
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
} }
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) {
case .text: case .text:
isMedia = false isMedia = false
default: default:

View File

@ -1019,7 +1019,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
displayLeave = false displayLeave = false
} }
result.append(.mute) result.append(.mute)
if hasVoiceChat { if hasVoiceChat || canStartVoiceChat {
result.append(.voiceChat) result.append(.voiceChat)
} }
if hasDiscussion { if hasDiscussion {
@ -1038,7 +1038,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) { if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
canReport = false canReport = false
} }
if !canReport && !canViewStats && !canStartVoiceChat { if !canReport && !canViewStats {
displayMore = false displayMore = false
} }
if displayMore { if displayMore {
@ -1051,10 +1051,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
var isPublic = false var isPublic = false
var isCreator = false var isCreator = false
var hasVoiceChat = false var hasVoiceChat = false
var canStartVoiceChat = false
if group.flags.contains(.hasVoiceChat) { if group.flags.contains(.hasVoiceChat) {
hasVoiceChat = true hasVoiceChat = true
} }
if !hasVoiceChat {
if case .creator = group.role {
canStartVoiceChat = true
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) {
canStartVoiceChat = true
}
}
if case .creator = group.role { if case .creator = group.role {
isCreator = true isCreator = true
@ -1073,13 +1081,11 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
if !group.hasBannedPermission(.banAddMembers) { if !group.hasBannedPermission(.banAddMembers) {
canAddMembers = true canAddMembers = true
} }
if canAddMembers { if canAddMembers {
result.append(.addMember) result.append(.addMember)
} }
result.append(.mute) result.append(.mute)
if hasVoiceChat { if hasVoiceChat || canStartVoiceChat {
result.append(.voiceChat) result.append(.voiceChat)
} }
result.append(.search) result.append(.search)

View File

@ -153,6 +153,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
colors = ["Middle.Group 1.Fill 1": iconColor, colors = ["Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor] "Line.Group 1.Stroke 1": iconColor]
if previousIcon == .unmute { if previousIcon == .unmute {
playOnce = true playOnce = true
@ -164,6 +165,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
colors = ["Middle.Group 1.Fill 1": iconColor, colors = ["Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor] "Line.Group 1.Stroke 1": iconColor]
if previousIcon == .mute { if previousIcon == .mute {
playOnce = true playOnce = true
@ -248,7 +250,9 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
if isActiveUpdated, !self.containerNode.alpha.isZero { if isActiveUpdated, !self.containerNode.alpha.isZero {
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3) alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3)
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3) if !isExpanded {
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
}
} }
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)

View File

@ -3371,7 +3371,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
case .videoCall: case .videoCall:
self.requestCall(isVideo: true) self.requestCall(isVideo: true)
case .voiceChat: case .voiceChat:
self.requestCall(isVideo: false) self.requestCall(isVideo: false, gesture: gesture)
case .mute: case .mute:
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState { if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start() let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
@ -3627,20 +3627,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
} }
} else if let channel = peer as? TelegramChannel { } else if let channel = peer as? TelegramChannel {
if !channel.flags.contains(.hasVoiceChat) {
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
self?.requestCall(isVideo: false, contextController: c, result: f, backAction: { c in
if let mainItemsImpl = mainItemsImpl {
c.setItems(mainItemsImpl())
}
})
})))
}
}
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) { if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
@ -3730,22 +3716,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
} }
} else if let group = peer as? TelegramGroup { } else if let group = peer as? TelegramGroup {
var canManageGroupCalls = false
if case .creator = group.role {
canManageGroupCalls = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canManageCalls) {
canManageGroupCalls = true
}
}
if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
self?.requestCall(isVideo: false, contextController: c, result: f)
})))
}
if case .Member = group.membership { if case .Member = group.membership {
if !items.isEmpty { if !items.isEmpty {
items.append(.separator) items.append(.separator)
@ -3976,14 +3946,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
}, activeCall: activeCall) }, activeCall: activeCall)
} else { } else {
if let defaultJoinAsPeerId = defaultJoinAsPeerId { self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: gesture, contextController: contextController)
result?(.dismissWithoutContent)
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
} else {
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId)
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
}
} }
} }
@ -4006,6 +3969,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {}) self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
} }
private func scheduleGroupCall() {
self.context.scheduleGroupCall(peerId: self.peerId)
//
//
// let time = Int32(Date().timeIntervalSince1970 + 86400)
// self.activeActionDisposable.set((createGroupCall(account: self.context.account, peerId: self.peerId, title: nil, scheduleDate: time)
// |> deliverOnMainQueue).start(next: { [weak self] info in
//
// }))
}
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) { private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
if let _ = self.context.sharedContext.callManager { if let _ = self.context.sharedContext.callManager {
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
@ -4013,26 +3987,41 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
return return
} }
var dismissStatus: (() -> Void)? var cancelImpl: (() -> Void)?
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { let presentationData = strongSelf.presentationData
dismissStatus?() let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
})) let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
dismissStatus = { [weak self, weak statusController] in cancelImpl?()
self?.activeActionDisposable.set(nil) }))
statusController?.dismiss() self?.controller?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
} }
strongSelf.controller?.present(statusController, in: .window(.root)) |> runOn(Queue.mainQueue())
strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId) |> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let createSignal = createGroupCall(account: strongSelf.context.account, peerId: peerId, title: nil, scheduleDate: nil)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
self?.activeActionDisposable.set(nil)
}
strongSelf.activeActionDisposable.set((createSignal
|> deliverOnMainQueue).start(next: { [weak self] info in |> deliverOnMainQueue).start(next: { [weak self] info in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
result(joinAsPeerId) result(joinAsPeerId)
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) }, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribed: false))
}, error: { [weak self] error in }, error: { [weak self] error in
dismissStatus?()
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -4046,8 +4035,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
} }
strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak self] in
dismissStatus?()
})) }))
} }
@ -4348,7 +4335,90 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
controller.push(statsController) controller.push(statsController)
} }
private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextController? = nil) {
let context = self.context
let peerId = self.peerId
let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}
let _ = (combineLatest(queue: Queue.mainQueue(), currentAccountPeer, self.displayAsPeersPromise.get() |> take(1))
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
var result = currentAccountPeer
result.append(contentsOf: availablePeers)
return result
}).start(next: { [weak self] peers in
guard let strongSelf = self else {
return
}
var items: [ContextMenuItem] = []
if peers.count > 1 {
var selectedPeer: FoundPeer?
for peer in peers {
if peer.peer.id == defaultJoinAsPeerId {
selectedPeer = peer
}
}
if let peer = selectedPeer {
let avatarSize = CGSize(width: 28.0, height: 28.0)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in
guard let strongSelf = self else {
return
}
strongSelf.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
let _ = updateGroupCallJoinAsPeer(account: context.account, peerId: peerId, joinAs: joinAsPeerId).start()
self?.openVoiceChatOptions(defaultJoinAsPeerId: joinAsPeerId, gesture: nil, contextController: c)
}, gesture: gesture, contextController: c, result: f, backAction: { [weak self] c in
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: nil, contextController: c)
})
})))
items.append(.separator)
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.dismissWithoutContent)
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_ScheduleVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.dismissWithoutContent)
self?.scheduleGroupCall()
})))
if let contextController = contextController {
contextController.setItems(.single(items))
} else {
strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller {
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture)
contextController.dismissed = { [weak self] in
if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
}
}
controller.presentInGlobalOverlay(contextController)
}
}
})
}
private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) { private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
let dismissOnSelection = contextController == nil
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map { peer in |> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)] return [FoundPeer(peer: peer, subscribers: nil)]
@ -4398,8 +4468,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
let avatarSize = CGSize(width: 28.0, height: 28.0) let avatarSize = CGSize(width: 28.0, height: 28.0)
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
f(.dismissWithoutContent) if dismissOnSelection {
f(.dismissWithoutContent)
}
completion(peer.peer.id) completion(peer.peer.id)
}))) })))
@ -7168,7 +7239,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController
} }
contactsController?.dismiss() contactsController?.dismiss()
},completed: { }, completed: {
contactsController?.dismiss() contactsController?.dismiss()
})) }))
})) }))

View File

@ -29,7 +29,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
var theme: PresentationTheme var theme: PresentationTheme
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
self.messageId = messageId self.messageId = messageId
self.theme = theme self.theme = theme
@ -86,7 +86,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder) authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
} }
if let message = message { if let message = message {
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
} }
var updatedMediaReference: AnyMediaReference? var updatedMediaReference: AnyMediaReference?
@ -152,7 +152,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
let isMedia: Bool let isMedia: Bool
if let message = message { if let message = message {
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) { switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) {
case .text: case .text:
isMedia = false isMedia = false
default: default: