mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat Scheduling
This commit is contained in:
parent
693aa7f106
commit
b9e52f27e3
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/VoiceStart.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/VoiceStart.tgs
Normal file
Binary file not shown.
Binary file not shown.
@ -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 %@";
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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()!
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
143
submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift
Normal file
143
submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
103
submodules/TelegramCallsUI/Sources/VoiceChatTitleNode.swift
Normal file
103
submodules/TelegramCallsUI/Sources/VoiceChatTitleNode.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "callshare (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/callshare (1).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/callshare (1).pdf
vendored
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}))
|
||||
}))
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user