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.UnpinVideo" = "Unpin Video";
"Notification.VoiceChatScheduled" = "Voice chat scheduled";
"Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
"VoiceChat.EditStartTime" = "Edit Start Time";
"VoiceChat.StartsIn" = "Starts in";
"VoiceChat.LateBy" = "Late by";
"VoiceChat.StartNow" = "Start Now";
"VoiceChat.SetReminder" = "Set Reminder";
"VoiceChat.CancelReminder" = "Cancel Reminder";
"VoiceChat.ShareShort" = "share";
"VoiceChat.TapToEditTitle" = "Tap to edit title";
"ChannelInfo.ScheduleVoiceChat" = "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.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@.";
"ScheduleVoiceChat.ScheduleToday" = "Remind today at %@";
"ScheduleVoiceChat.ScheduleTomorrow" = "Remind tomorrow at %@";
"ScheduleVoiceChat.ScheduleOn" = "Remind on %@ at %@";
"ScheduleVoiceChat.ScheduleToday" = "Start today at %@";
"ScheduleVoiceChat.ScheduleTomorrow" = "Start tomorrow at %@";
"ScheduleVoiceChat.ScheduleOn" = "Start on %@ at %@";
"VoiceChat.ScheduledTitle" = "Scheduled Voice Chat";
"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat";
"Conversation.ScheduledVoiceChatStartsInShort" = "Voice chat starts %@";
"Conversation.ScheduledVoiceChatStartsInShort" = "Starts %@";
"Conversation.ScheduledVoiceChatStartsOn" = "Voice chat 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 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 requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
}

View File

@ -17,6 +17,11 @@ public enum JoinGroupCallManagerResult {
case alreadyInProgress(PeerId?)
}
public enum RequestScheduleGroupCallResult {
case success
case alreadyInProgress(PeerId?)
}
public struct CallAuxiliaryServer {
public enum Connection {
case stun
@ -181,6 +186,7 @@ public struct PresentationGroupCallState: Equatable {
public var recordingStartTimestamp: Int32?
public var title: String?
public var raisedHand: Bool
public var scheduleTimestamp: Int32?
public init(
myPeerId: PeerId,
@ -191,7 +197,8 @@ public struct PresentationGroupCallState: Equatable {
defaultParticipantMuteState: DefaultParticipantMuteState?,
recordingStartTimestamp: Int32?,
title: String?,
raisedHand: Bool
raisedHand: Bool,
scheduleTimestamp: Int32?
) {
self.myPeerId = myPeerId
self.networkState = networkState
@ -202,6 +209,7 @@ public struct PresentationGroupCallState: Equatable {
self.recordingStartTimestamp = recordingStartTimestamp
self.title = title
self.raisedHand = raisedHand
self.scheduleTimestamp = scheduleTimestamp
}
}
@ -299,6 +307,8 @@ public protocol PresentationGroupCall: class {
var isVideo: Bool { get }
var schedulePending: Bool { get }
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
var canBeRemoved: Signal<Bool, NoError> { get }
@ -313,6 +323,9 @@ public protocol PresentationGroupCall: class {
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
var reconnectedAsEvents: Signal<Peer, NoError> { get }
func schedule(timestamp: Int32)
func startScheduled()
func reconnect(with invite: String)
func reconnect(as peerId: PeerId)
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 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 {
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 {
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 {
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 {
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
switch contentPeer {
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 {
initialHideAuthor = true

View File

@ -46,7 +46,7 @@ private func messageGroupType(messages: [Message]) -> MessageGroupType {
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 message = messages.last
@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
}
default:
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
}
}
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
}
case let poll as TelegramMediaPoll:

View File

@ -569,12 +569,15 @@ final class ContextActionsContainerNode: ASDisplayNode {
}
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let additionalActionsNode = self.additionalActionsNode else {
guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
return
}
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)
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)
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 transition.isAnimated {
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions {
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true {
var initialFrame = self.actionsContainerNode.frame
let delta = (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 dismissed: (() -> Void)?
public var useComplexItemsTransitionAnimation = false
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) {

View File

@ -383,7 +383,12 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U
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 {
return nil
}
@ -395,7 +400,7 @@ public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [C
var 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()!

View File

@ -907,7 +907,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var generalMessageContentKind: MessageContentKind?
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 {
generalMessageContentKind = currentKind
} else {
@ -1056,7 +1056,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var messageContentKinds = Set<MessageContentKindKey>()
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 {
generalMessageContentKind = nil
} 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) {
self.intrinsicSize = size
@ -286,4 +292,11 @@ open class ManagedAnimationNode: ASDisplayNode {
self.didTryAdvancingState = false
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 text: String
var actions: [TextAlertAction] = [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]
var actions: [TextAlertAction] = []
switch error {
case .limitExceeded:
text = presentationData.strings.Login_CodeFloodError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .invalidPhoneNumber:
text = presentationData.strings.Login_InvalidPhoneError
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .phoneNumberOccupied:
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
case .phoneBanned:
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 {
return
}
@ -162,6 +164,7 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
}))
case .generic:
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))

View File

@ -3,8 +3,6 @@ import UIKit
import AsyncDisplayKit
import Display
private let textFont: UIFont = Font.regular(16.0)
public final class SolidRoundedButtonTheme {
public let backgroundColor: 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 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
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 presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
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()))

View File

@ -333,7 +333,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 {
panelData = nil
} 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
@ -406,7 +406,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
strongSelf.joinGroupCall(
peerId: groupCallPanelData.peerId,
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 {

View File

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

View File

@ -15,11 +15,20 @@ private let blue = UIColor(rgb: 0x0078ff)
private let lightBlue = UIColor(rgb: 0x59c7f8)
private let green = UIColor(rgb: 0x33c659)
private let activeBlue = UIColor(rgb: 0x00a0b9)
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
private class CallStatusBarBackgroundNode: ASDisplayNode {
enum State {
case connecting
case cantSpeak
case active
case speaking
}
private let foregroundView: UIView
private let foregroundGradientLayer: CAGradientLayer
private let maskCurveView: VoiceCurveView
private let initialTimestamp = CACurrentMediaTime()
var audioLevel: Float = 0.0 {
didSet {
@ -35,9 +44,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
}
}
var speaking: Bool? = nil {
var state: State = .connecting {
didSet {
if self.speaking != oldValue {
if self.state != oldValue {
self.updateGradientColors()
}
}
@ -46,13 +55,26 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
private func updateGradientColors() {
let initialColors = self.foregroundGradientLayer.colors
let targetColors: [CGColor]
if let speaking = self.speaking {
targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor]
} else {
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
switch self.state {
case .connecting:
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
@ -177,6 +199,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
private var currentCallState: PresentationCallState?
private var currentGroupCallState: PresentationGroupCallSummaryState?
private var currentIsMuted = true
private var currentCantSpeak = false
private var currentMembers: PresentationGroupCallMembers?
private var currentIsConnected = true
@ -279,16 +302,24 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
strongSelf.currentMembers = members
var isMuted = isMuted
var cantSpeak = false
if let state = state, let muteState = state.callState.muteState {
if !muteState.canUnmute {
isMuted = true
cantSpeak = true
}
}
if state?.callState.scheduleTimestamp != nil {
cantSpeak = true
}
strongSelf.currentIsMuted = isMuted
strongSelf.currentCantSpeak = cantSpeak
let currentIsConnected: Bool
if let state = state, case .connected = state.callState.networkState {
currentIsConnected = true
} else if state?.callState.scheduleTimestamp != nil {
currentIsConnected = true
} else {
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.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))
}
}

View File

@ -7,12 +7,29 @@ import SyncCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import AccountContext
import AppBundle
import SwiftSignalKit
import AnimatedAvatarSetNode
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 subtitleFont = Font.regular(13.0)
@ -79,6 +96,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private let context: AccountContext
private var theme: PresentationTheme
private var strings: PresentationStrings
private var dateTimeFormat: PresentationDateTimeFormat
private let tapAction: () -> Void
@ -102,6 +120,10 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private var textIsActive = false
private let muteIconNode: ASImageNode
private var isScheduled = false
private var currentText: String = ""
private var updateTimer: SwiftSignalKit.Timer?
private let avatarsContext: AnimatedAvatarSetContext
private var avatarsContent: AnimatedAvatarSetContext.Content?
private let avatarsNode: AnimatedAvatarSetNode
@ -125,6 +147,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat
self.tapAction = tapAction
@ -135,6 +158,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.joinButton = HighlightableButtonNode()
self.joinButtonTitleNode = ImmediateTextNode()
self.joinButtonBackgroundNode = ASImageNode()
self.joinButtonBackgroundNode.clipsToBounds = true
self.joinButtonBackgroundNode.displaysAsynchronously = false
self.joinButtonBackgroundNode.cornerRadius = 14.0
self.micButton = HighlightTrackingButtonNode()
self.micButtonForegroundNode = VoiceChatMicrophoneNode()
@ -198,6 +224,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.membersDisposable.dispose()
self.isMutedDisposable.dispose()
self.audioLevelGeneratorTimer?.invalidate()
self.updateTimer?.invalidate()
}
public override func didLoad() {
@ -250,6 +277,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
public func updatePresentationData(_ presentationData: PresentationData) {
self.theme = presentationData.theme
self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat
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.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor)
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.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.updateJoinButton()
if let (size, leftInset, rightInset) = self.validLayout {
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() {
if let snapshotView = self.textNode.view.snapshotContentTree() {
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
@ -298,6 +339,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else {
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)
@ -321,9 +363,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else {
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
}
strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
strongSelf.currentText = membersText
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
if let (size, leftInset, rightInset) = strongSelf.validLayout {
@ -382,7 +423,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
strongSelf.micButton.view.insertSubview(audioLevelView, at: 0)
}
let level = min(1.0, max(0.0, CGFloat(value)))
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
if value > 0.0 {
strongSelf.audioLevelView?.startAnimating()
@ -400,9 +440,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
} else {
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)
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))
}
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 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)
@ -500,15 +590,17 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
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)
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 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 {
let begin: () -> Void = { [weak self] in
if let requestJoinAsPeerId = requestJoinAsPeerId {

View File

@ -77,6 +77,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
clientParams: nil,
streamDcId: nil,
title: call.title,
scheduleTimestamp: call.scheduleTimestamp,
recordingStartTimestamp: nil,
sortAscending: true
),
@ -120,7 +121,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
}
return GroupCallPanelData(
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,
participantCount: state.totalCount,
activeSpeakers: activeSpeakers,
@ -205,7 +206,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach
}
private extension PresentationGroupCallState {
static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState {
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?) -> PresentationGroupCallState {
return PresentationGroupCallState(
myPeerId: myPeerId,
networkState: .connecting,
@ -215,7 +216,8 @@ private extension PresentationGroupCallState {
defaultParticipantMuteState: nil,
recordingStartTimestamp: nil,
title: title,
raisedHand: false
raisedHand: false,
scheduleTimestamp: scheduleTimestamp
)
}
}
@ -508,6 +510,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let joinDisposable = MetaDisposable()
private let requestDisposable = MetaDisposable()
private let startDisposable = MetaDisposable()
private var groupCallParticipantUpdatesDisposable: Disposable?
private let networkStateDisposable = MetaDisposable()
@ -550,6 +553,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var peerUpdatesSubscription: Disposable?
public private(set) var schedulePending = false
init(
accountContext: AccountContext,
audioSession: ManagedAudioSession,
@ -572,8 +577,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.peerId = peerId
self.invite = invite
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.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
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)
} else {
@ -805,7 +811,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.stateValue = updatedValue
})
self.requestCall(movingFromBroadcastToRtc: false)
if let _ = self.initialCall {
self.requestCall(movingFromBroadcastToRtc: false)
}
}
deinit {
@ -815,6 +823,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.audioSessionDisposable?.dispose()
self.joinDisposable.dispose()
self.requestDisposable.dispose()
self.startDisposable.dispose()
self.groupCallParticipantUpdatesDisposable?.dispose()
self.leaveDisposable.dispose()
self.isMutedDisposable.dispose()
@ -1039,287 +1048,301 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
var shouldJoin = false
let activeCallInfo: GroupCallInfo?
switch previousInternalState {
case .active:
break
default:
if case let .active(callInfo) = internalState {
let callContext: OngoingGroupCallContext
if let current = self.callContext {
callContext = current
case let .active(previousCallInfo):
if case let .active(callInfo) = internalState {
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
activeCallInfo = callInfo
} 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
}
activeCallInfo = nil
}
default:
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
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
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
Queue.mainQueue().async {
guard let strongSelf = self else {
return [:]
return
}
var result: [PeerId: UInt32] = [:]
for source in sources {
if let peerId = strongSelf.ssrcMapping[source] {
result[peerId] = source
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 {
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
})
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
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
}
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)
}
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
}
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
}
}).start()
}
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()
}))
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
|> 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
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 strongSelf.isCurrentlyConnecting != isConnecting {
strongSelf.isCurrentlyConnecting = 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.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)
}))
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
self.audioLevelsDisposable.set((callContext.audioLevels
|> deliverOnMainQueue).start(next: { [weak self] levels in
guard let strongSelf = self else {
return
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
if isConnecting {
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
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
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
}
}
if isConnecting {
strongSelf.didStartConnectingOnce = true
}
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.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
|> 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)
let mappedLevel = myLevel * 1.5
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
if !missingSsrcs.isEmpty {
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
}
}))
}
}
strongSelf.speakingParticipantsContext.update(levels: result)
let mappedLevel = myLevel * 1.5
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
if !missingSsrcs.isEmpty {
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
}
}))
}
switch previousInternalState {
@ -1339,6 +1362,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if 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 peerId = self.peerId
@ -1630,6 +1656,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
strongSelf.stateValue.title = state.title
strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
id: callInfo.id,
@ -1638,6 +1665,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
clientParams: nil,
streamDcId: nil,
title: state.title,
scheduleTimestamp: state.scheduleTimestamp,
recordingStartTimestamp: state.recordingStartTimestamp,
sortAscending: state.sortAscending
))))
@ -1887,7 +1915,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
self.leaving = true
if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc {
if let callInfo = self.internalState.callInfo {
if terminateIfPossible {
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|> deliverOnMainQueue).start(completed: { [weak self] in
@ -1896,7 +1924,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
strongSelf.markAsCanBeRemoved()
}))
} else {
} else if let localSsrc = self.currentLocalSsrc {
if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl {
let account = self.account
let id = callInfo.id
@ -1907,6 +1935,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
self.markAsCanBeRemoved()
} else {
self.markAsCanBeRemoved()
}
} else {
self.markAsCanBeRemoved()
@ -1957,6 +1987,39 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
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() {
guard let membersValue = self.membersValue else {
return
@ -2207,7 +2270,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
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)
} else {
@ -2217,7 +2280,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
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
}
@ -2236,11 +2299,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.invitedPeersValue = updatedInvitedPeers
}
public func updateTitle(_ title: String){
guard case let .established(callInfo, _, _, _, _) = self.internalState else {
public func updateTitle(_ title: String) {
guard let callInfo = self.internalState.callInfo else {
return
}
self.stateValue.title = title
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 smallIconScale: CGFloat = 0.69
private let buttonHeight: CGFloat = 52.0
final class VoiceChatActionButton: HighlightTrackingButtonNode {
enum State: Equatable {
enum ActiveState: Equatable {
@ -34,7 +36,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
case muted
case on
}
enum ScheduledState: Equatable {
case start
case subscribe
case unsubscribe
}
case button(text: String)
case scheduled(state: ScheduledState)
case connecting
case active(state: ActiveState)
}
@ -53,6 +63,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
private let iconNode: VoiceChatActionButtonIconNode
private let titleLabel: 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)?
@ -103,7 +114,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
default:
break
}
case .connecting:
case .connecting, .button, .scheduled:
break
}
} else {
@ -121,12 +132,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
init() {
self.bottomNode = ASDisplayNode()
self.bottomNode.isUserInteractionEnabled = false
self.containerNode = ASDisplayNode()
self.containerNode.isUserInteractionEnabled = false
self.backgroundNode = VoiceChatActionButtonBackgroundNode()
self.iconNode = VoiceChatActionButtonIconNode(isColored: false)
self.titleLabel = ImmediateTextNode()
self.subtitleLabel = ImmediateTextNode()
self.buttonTitleLabel = ImmediateTextNode()
self.buttonTitleLabel.isUserInteractionEnabled = false
self.buttonTitleLabel.alpha = 0.0
super.init()
@ -138,26 +154,38 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.buttonTitleLabel)
self.highligthedChanged = { [weak self] pressing in
if let strongSelf = self {
guard let (_, _, _, _, small, _, _, snap) = strongSelf.currentParams else {
guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else {
return
}
if pressing {
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)
if case .button = state {
strongSelf.containerNode.layer.removeAnimation(forKey: "opacity")
strongSelf.containerNode.alpha = 0.4
} 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 {
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)
if case .button = state {
strongSelf.containerNode.alpha = 1.0
strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
} 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 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.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
@ -232,7 +260,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
default:
break
}
case .connecting:
case .connecting, .button, .scheduled:
break
}
@ -271,6 +299,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let icon: VoiceChatActionButtonIconAnimationState
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):
switch state {
case .on:
@ -290,7 +329,6 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.previousIcon = 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) {
@ -312,8 +350,26 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
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
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):
switch state {
case .on:
@ -340,14 +396,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
}
}))
} else {
applyParams(animated: animated)
self.applyParams(animated: animated)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var hitRect = self.bounds
if let (_, buttonSize, _, _, _, _, _, _) = self.currentParams {
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams {
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)
if !hitRect.contains(point) {
@ -453,6 +513,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
enum State: Equatable {
case connecting
case disabled
case button
case blob(Bool)
}
@ -546,7 +607,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskProgressLayer.lineCap = .round
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.path = largerCirclePath
self.maskCircleLayer.isHidden = true
@ -825,7 +888,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskBlobView.startAnimating()
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) {
CATransaction.begin()
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)
if case .blob = self.state {
if case .connecting = self.state {
} else {
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)
@ -907,6 +971,47 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
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
func updateAnimations() {
if !self.isCurrentlyInHierarchy {
@ -959,7 +1064,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.isActive = false
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?.isActive = false
}
@ -969,7 +1076,10 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
}
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() {
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.foregroundCircleLayer.position = center
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
self.growingForegroundCircleLayer.position = center
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.foregroundView.frame = self.bounds
self.foregroundGradientLayer.frame = self.bounds
self.maskGradientLayer.position = center
self.maskGradientLayer.bounds = self.bounds
self.maskGradientLayer.bounds = bounds
self.maskView.frame = self.bounds
}
}
@ -1386,6 +1500,10 @@ final class BlobView: UIView {
}
enum VoiceChatActionButtonIconAnimationState: Equatable {
case empty
case start
case subscribe
case unsubscribe
case unmute
case mute
case hand
@ -1399,6 +1517,7 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
self.isColored = isColored
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))
}
@ -1410,30 +1529,73 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
let previousState = self.iconState
self.iconState = state
if state != .empty {
self.alpha = 1.0
}
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:
switch state {
case .mute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute")))
case .hand:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2")))
case .unmute:
default:
break
}
case .mute:
switch state {
case .start:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
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:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff")))
case .mute:
case .empty:
self.alpha = 0.0
default:
break
}
case .hand:
switch state {
case .mute, .unmute:
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn")))
case .hand:
default:
break
}
}

View File

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

View File

@ -145,7 +145,7 @@ public final class VoiceChatJoinScreen: ViewController {
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 {
strongSelf.dismiss()
strongSelf.join(activeCall)

View File

@ -396,7 +396,7 @@ public final class VoiceChatOverlayController: ViewController {
var slide = true
var hidden = true
var animated = true
var animateInsets = true
if controllers.count == 1 || controllers.last is ChatController {
if let chatController = controllers.last as? ChatController {
slide = false
@ -416,9 +416,13 @@ public final class VoiceChatOverlayController: ViewController {
hidden = true
}
if case .active(.cantSpeak) = state {
hidden = true
switch state {
case .active(.cantSpeak), .button, .scheduled:
hidden = true
default:
break
}
if hasVoiceChatController {
hidden = false
animated = self.initiallyHidden
@ -429,7 +433,6 @@ public final class VoiceChatOverlayController: ViewController {
let previousInsets = self.additionalSideInsets
self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
if previousInsets != self.additionalSideInsets {
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
init(theme: PresentationTheme, placeholder: String, maxLength: Int) {
init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
self.theme = theme
self.maxLength = maxLength
@ -65,7 +65,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = .default
self.textInputNode.autocapitalizationType = .sentences
self.textInputNode.returnKeyType = .done
self.textInputNode.returnKeyType = returnKeyType
self.textInputNode.autocorrectionType = .default
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
@ -510,7 +510,7 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
self.titleNode = ASTextNode()
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.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
@ -550,14 +550,6 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
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)
}

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
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
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 {
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 {
return cachedData
}
@ -331,15 +331,27 @@ public func updateGroupCallJoinAsPeer(account: Account, peerId: PeerId, joinAs:
}
|> castError(UpdateGroupCallJoinAsPeerError.self)
|> mapToSignal { result in
guard let (peer, joinAs) = result else {
guard let (inputPeer, joinInputPeer) = result else {
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
return .generic
}
|> 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
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
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 {
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 {
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 {
if let attribute = attribute as? RestrictedContentMessageAttribute {
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 {
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 .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 {
case let expiredMedia as TelegramMediaExpiredContent:
switch expiredMedia.data {
@ -163,7 +163,7 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P
}
case _ as TelegramMediaAction:
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 {
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) {
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId)
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, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
return (foldLineBreaks(message.text), false)
}

View File

@ -27,11 +27,11 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P
return result
}
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.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, 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?
let primaryTextColor: UIColor
@ -448,7 +448,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
case let .groupPhoneCall(_, _, scheduleDate, duration):
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)
} else if let duration = duration {
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) {
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 {

View File

@ -356,7 +356,7 @@ final class AuthorizedApplicationContext {
if inAppNotificationSettings.displayPreviews {
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 {
var foundOverlay = false
strongSelf.mainWindow.forEachViewController({ controller in

View File

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

View File

@ -535,7 +535,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case .groupPhoneCall, .inviteToGroupPhoneCall:
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 {
var canManageGroupCalls = false
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
@ -564,12 +564,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
statusController?.dismiss()
}
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
guard let strongSelf = self else {
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
dismissStatus?()

View File

@ -32,7 +32,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return editPanelNode
} 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
return panelNode
}
@ -63,7 +63,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return replyPanelNode
} 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
return panelNode
}

View File

@ -18,8 +18,8 @@ import UniversalMediaPlayer
import TelegramUniversalVideoContent
import GalleryUI
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false)
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, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false)
}
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
@ -132,7 +132,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
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?
for media in item.message.media {

View File

@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData {
if let chatPeer = message.peers[item.message.id.peerId] {
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

View File

@ -18,6 +18,7 @@ import TelegramStringFormatting
public final class ChatMessageNotificationItem: NotificationItem {
let context: AccountContext
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let messages: [Message]
let tapAction: () -> Bool
@ -27,9 +28,10 @@ public final class ChatMessageNotificationItem: NotificationItem {
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.strings = strings
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.messages = messages
self.tapAction = tapAction
@ -181,7 +183,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
if message.containsSecretMedia {
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] {
var displayAuthor = true
if let channel = peer as? TelegramChannel {
@ -218,9 +220,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
}
}
} 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 {
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 {
kind = .text
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 titleColor: UIColor

View File

@ -269,7 +269,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.currentMessage = interfaceState.pinnedMessage
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)
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
}
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
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 (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 {
if let strongSelf = self {

View File

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

View File

@ -15,6 +15,7 @@ import PhotoResources
import TelegramStringFormatting
final class EditAccessoryPanelNode: AccessoryPanelNode {
let dateTimeFormat: PresentationDateTimeFormat
let messageId: MessageId
let closeButton: ASButtonNode
@ -67,12 +68,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
var strings: PresentationStrings
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.messageId = messageId
self.theme = theme
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
self.closeButton = ASButtonNode()
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
@ -159,7 +161,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
if let currentEditMediaReference = self.currentEditMediaReference {
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?
@ -231,7 +233,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
if let currentEditMediaReference = self.currentEditMediaReference {
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:
isMedia = false
default:

View File

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

View File

@ -153,6 +153,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
colors = ["Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor]
if previousIcon == .unmute {
playOnce = true
@ -164,6 +165,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
colors = ["Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor]
if previousIcon == .mute {
playOnce = true
@ -248,7 +250,9 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
if isActiveUpdated, !self.containerNode.alpha.isZero {
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
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)

View File

@ -3371,7 +3371,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
case .videoCall:
self.requestCall(isVideo: true)
case .voiceChat:
self.requestCall(isVideo: false)
self.requestCall(isVideo: false, gesture: gesture)
case .mute:
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
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 {
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) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
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 {
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 !items.isEmpty {
items.append(.separator)
@ -3976,14 +3946,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
}, activeCall: activeCall)
} else {
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
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)
}
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: gesture, contextController: contextController)
}
}
@ -4006,6 +3969,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
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?) {
if let _ = self.context.sharedContext.callManager {
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
@ -4013,26 +3987,41 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
return
}
var dismissStatus: (() -> Void)?
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
dismissStatus?()
}))
dismissStatus = { [weak self, weak statusController] in
self?.activeActionDisposable.set(nil)
statusController?.dismiss()
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.controller?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
strongSelf.controller?.present(statusController, in: .window(.root))
strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId)
|> runOn(Queue.mainQueue())
|> 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
guard let strongSelf = self else {
return
}
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
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
dismissStatus?()
guard let strongSelf = self else {
return
}
@ -4046,8 +4035,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
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))
}, completed: { [weak self] in
dismissStatus?()
}))
}
@ -4348,7 +4335,90 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
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) {
let dismissOnSelection = contextController == nil
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map { peer in
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 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
f(.dismissWithoutContent)
if dismissOnSelection {
f(.dismissWithoutContent)
}
completion(peer.peer.id)
})))
@ -7168,7 +7239,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController
}
contactsController?.dismiss()
},completed: {
}, completed: {
contactsController?.dismiss()
}))
}))

View File

@ -29,7 +29,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
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.theme = theme
@ -86,7 +86,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
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?
@ -152,7 +152,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
let isMedia: Bool
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:
isMedia = false
default: