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.PinVideo" = "Pin Video";
|
||||||
"VoiceChat.UnpinVideo" = "Unpin Video";
|
"VoiceChat.UnpinVideo" = "Unpin Video";
|
||||||
|
|
||||||
"Notification.VoiceChatScheduled" = "Voice chat scheduled";
|
"Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
|
||||||
|
|
||||||
"VoiceChat.EditStartTime" = "Edit Start Time";
|
|
||||||
"VoiceChat.StartsIn" = "Starts in";
|
"VoiceChat.StartsIn" = "Starts in";
|
||||||
|
"VoiceChat.LateBy" = "Late by";
|
||||||
|
|
||||||
|
"VoiceChat.StartNow" = "Start Now";
|
||||||
"VoiceChat.SetReminder" = "Set Reminder";
|
"VoiceChat.SetReminder" = "Set Reminder";
|
||||||
"VoiceChat.CancelReminder" = "Cancel Reminder";
|
"VoiceChat.CancelReminder" = "Cancel Reminder";
|
||||||
|
|
||||||
"VoiceChat.ShareShort" = "share";
|
"VoiceChat.ShareShort" = "share";
|
||||||
|
|
||||||
|
"VoiceChat.TapToEditTitle" = "Tap to edit title";
|
||||||
|
|
||||||
"ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat";
|
"ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat";
|
||||||
|
|
||||||
"ScheduleVoiceChat.Title" = "Schedule Voice Chat";
|
"ScheduleVoiceChat.Title" = "Schedule Voice Chat";
|
||||||
"ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@.";
|
"ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@.";
|
||||||
"ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@.";
|
"ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@.";
|
||||||
|
|
||||||
"ScheduleVoiceChat.ScheduleToday" = "Remind today at %@";
|
"ScheduleVoiceChat.ScheduleToday" = "Start today at %@";
|
||||||
"ScheduleVoiceChat.ScheduleTomorrow" = "Remind tomorrow at %@";
|
"ScheduleVoiceChat.ScheduleTomorrow" = "Start tomorrow at %@";
|
||||||
"ScheduleVoiceChat.ScheduleOn" = "Remind on %@ at %@";
|
"ScheduleVoiceChat.ScheduleOn" = "Start on %@ at %@";
|
||||||
|
|
||||||
|
"VoiceChat.ScheduledTitle" = "Scheduled Voice Chat";
|
||||||
|
|
||||||
"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat";
|
"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat";
|
||||||
"Conversation.ScheduledVoiceChatStartsInShort" = "Voice chat starts %@";
|
"Conversation.ScheduledVoiceChatStartsOn" = "Voice chat starts %@";
|
||||||
"Conversation.ScheduledVoiceChatStartsInShort" = "Starts %@";
|
"Conversation.ScheduledVoiceChatStartsOnShort" = "Starts %@";
|
||||||
|
@ -736,6 +736,7 @@ public protocol AccountContext: class {
|
|||||||
func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
|
func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
|
||||||
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
||||||
|
|
||||||
|
func scheduleGroupCall(peerId: PeerId)
|
||||||
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall)
|
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall)
|
||||||
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
|
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,11 @@ public enum JoinGroupCallManagerResult {
|
|||||||
case alreadyInProgress(PeerId?)
|
case alreadyInProgress(PeerId?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum RequestScheduleGroupCallResult {
|
||||||
|
case success
|
||||||
|
case alreadyInProgress(PeerId?)
|
||||||
|
}
|
||||||
|
|
||||||
public struct CallAuxiliaryServer {
|
public struct CallAuxiliaryServer {
|
||||||
public enum Connection {
|
public enum Connection {
|
||||||
case stun
|
case stun
|
||||||
@ -181,6 +186,7 @@ public struct PresentationGroupCallState: Equatable {
|
|||||||
public var recordingStartTimestamp: Int32?
|
public var recordingStartTimestamp: Int32?
|
||||||
public var title: String?
|
public var title: String?
|
||||||
public var raisedHand: Bool
|
public var raisedHand: Bool
|
||||||
|
public var scheduleTimestamp: Int32?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
myPeerId: PeerId,
|
myPeerId: PeerId,
|
||||||
@ -191,7 +197,8 @@ public struct PresentationGroupCallState: Equatable {
|
|||||||
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
||||||
recordingStartTimestamp: Int32?,
|
recordingStartTimestamp: Int32?,
|
||||||
title: String?,
|
title: String?,
|
||||||
raisedHand: Bool
|
raisedHand: Bool,
|
||||||
|
scheduleTimestamp: Int32?
|
||||||
) {
|
) {
|
||||||
self.myPeerId = myPeerId
|
self.myPeerId = myPeerId
|
||||||
self.networkState = networkState
|
self.networkState = networkState
|
||||||
@ -202,6 +209,7 @@ public struct PresentationGroupCallState: Equatable {
|
|||||||
self.recordingStartTimestamp = recordingStartTimestamp
|
self.recordingStartTimestamp = recordingStartTimestamp
|
||||||
self.title = title
|
self.title = title
|
||||||
self.raisedHand = raisedHand
|
self.raisedHand = raisedHand
|
||||||
|
self.scheduleTimestamp = scheduleTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +307,8 @@ public protocol PresentationGroupCall: class {
|
|||||||
|
|
||||||
var isVideo: Bool { get }
|
var isVideo: Bool { get }
|
||||||
|
|
||||||
|
var schedulePending: Bool { get }
|
||||||
|
|
||||||
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
|
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
|
||||||
|
|
||||||
var canBeRemoved: Signal<Bool, NoError> { get }
|
var canBeRemoved: Signal<Bool, NoError> { get }
|
||||||
@ -313,6 +323,9 @@ public protocol PresentationGroupCall: class {
|
|||||||
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
|
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
|
||||||
var reconnectedAsEvents: Signal<Peer, NoError> { get }
|
var reconnectedAsEvents: Signal<Peer, NoError> { get }
|
||||||
|
|
||||||
|
func schedule(timestamp: Int32)
|
||||||
|
func startScheduled()
|
||||||
|
|
||||||
func reconnect(with invite: String)
|
func reconnect(with invite: String)
|
||||||
func reconnect(as peerId: PeerId)
|
func reconnect(as peerId: PeerId)
|
||||||
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
||||||
@ -355,4 +368,5 @@ public protocol PresentationCallManager: class {
|
|||||||
|
|
||||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||||
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||||
|
func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
|
||||||
}
|
}
|
||||||
|
@ -518,7 +518,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else {
|
} else {
|
||||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||||
}
|
}
|
||||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
||||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
||||||
}
|
}
|
||||||
@ -552,7 +552,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else {
|
} else {
|
||||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||||
}
|
}
|
||||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
||||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
||||||
}
|
}
|
||||||
@ -958,7 +958,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var hideAuthor = false
|
var hideAuthor = false
|
||||||
switch contentPeer {
|
switch contentPeer {
|
||||||
case let .chat(itemPeer):
|
case let .chat(itemPeer):
|
||||||
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
||||||
|
|
||||||
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
|
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
|
||||||
initialHideAuthor = true
|
initialHideAuthor = true
|
||||||
|
@ -46,7 +46,7 @@ private func messageGroupType(messages: [Message]) -> MessageGroupType {
|
|||||||
return currentType
|
return currentType
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) {
|
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) {
|
||||||
let peer: Peer?
|
let peer: Peer?
|
||||||
|
|
||||||
let message = messages.last
|
let message = messages.last
|
||||||
@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
hideAuthor = true
|
hideAuthor = true
|
||||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||||
messageText = text
|
messageText = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case _ as TelegramMediaExpiredContent:
|
case _ as TelegramMediaExpiredContent:
|
||||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||||
messageText = text
|
messageText = text
|
||||||
}
|
}
|
||||||
case let poll as TelegramMediaPoll:
|
case let poll as TelegramMediaPoll:
|
||||||
|
@ -569,12 +569,15 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
guard let additionalActionsNode = self.additionalActionsNode else {
|
guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||||
|
transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||||
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||||
|
additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1562,10 +1562,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if let previousActionsContainerNode = previousActionsContainerNode {
|
if let previousActionsContainerNode = previousActionsContainerNode {
|
||||||
if transition.isAnimated {
|
if transition.isAnimated {
|
||||||
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions {
|
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true {
|
||||||
var initialFrame = self.actionsContainerNode.frame
|
var initialFrame = self.actionsContainerNode.frame
|
||||||
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
|
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
|
||||||
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
|
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
|
||||||
@ -1773,6 +1772,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
|
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
|
||||||
public var dismissed: (() -> Void)?
|
public var dismissed: (() -> Void)?
|
||||||
|
|
||||||
|
public var useComplexItemsTransitionAnimation = false
|
||||||
|
|
||||||
private var shouldBeDismissedDisposable: Disposable?
|
private var shouldBeDismissedDisposable: Disposable?
|
||||||
|
|
||||||
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) {
|
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) {
|
||||||
|
@ -383,7 +383,12 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U
|
|||||||
return tintedImage
|
return tintedImage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat]) -> UIImage? {
|
public enum GradientImageDirection {
|
||||||
|
case vertical
|
||||||
|
case horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? {
|
||||||
guard colors.count == locations.count else {
|
guard colors.count == locations.count else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -395,7 +400,7 @@ public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [C
|
|||||||
var locations = locations
|
var locations = locations
|
||||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||||
|
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||||
}
|
}
|
||||||
|
|
||||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||||
|
@ -907,7 +907,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
|
|
||||||
var generalMessageContentKind: MessageContentKind?
|
var generalMessageContentKind: MessageContentKind?
|
||||||
for message in messages {
|
for message in messages {
|
||||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId)
|
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
||||||
if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
|
if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
|
||||||
generalMessageContentKind = currentKind
|
generalMessageContentKind = currentKind
|
||||||
} else {
|
} else {
|
||||||
@ -1056,7 +1056,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
var messageContentKinds = Set<MessageContentKindKey>()
|
var messageContentKinds = Set<MessageContentKindKey>()
|
||||||
|
|
||||||
for message in messages {
|
for message in messages {
|
||||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId)
|
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
||||||
if beganContentKindScanning && currentKind != generalMessageContentKind {
|
if beganContentKindScanning && currentKind != generalMessageContentKind {
|
||||||
generalMessageContentKind = nil
|
generalMessageContentKind = nil
|
||||||
} else if !beganContentKindScanning || currentKind == generalMessageContentKind {
|
} else if !beganContentKindScanning || currentKind == generalMessageContentKind {
|
||||||
|
@ -145,6 +145,12 @@ open class ManagedAnimationNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var scale: CGFloat = 1.0 {
|
||||||
|
didSet {
|
||||||
|
self.imageNode.transform = CATransform3DMakeScale(self.scale, self.scale, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public init(size: CGSize) {
|
public init(size: CGSize) {
|
||||||
self.intrinsicSize = size
|
self.intrinsicSize = size
|
||||||
|
|
||||||
@ -286,4 +292,11 @@ open class ManagedAnimationNode: ASDisplayNode {
|
|||||||
self.didTryAdvancingState = false
|
self.didTryAdvancingState = false
|
||||||
self.updateAnimation()
|
self.updateAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open override func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
self.imageNode.bounds = self.bounds
|
||||||
|
self.imageNode.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,19 +135,21 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
|
|||||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
var actions: [TextAlertAction] = [
|
var actions: [TextAlertAction] = []
|
||||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
|
||||||
]
|
|
||||||
switch error {
|
switch error {
|
||||||
case .limitExceeded:
|
case .limitExceeded:
|
||||||
text = presentationData.strings.Login_CodeFloodError
|
text = presentationData.strings.Login_CodeFloodError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
case .invalidPhoneNumber:
|
case .invalidPhoneNumber:
|
||||||
text = presentationData.strings.Login_InvalidPhoneError
|
text = presentationData.strings.Login_InvalidPhoneError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
case .phoneNumberOccupied:
|
case .phoneNumberOccupied:
|
||||||
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0
|
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
case .phoneBanned:
|
case .phoneBanned:
|
||||||
text = presentationData.strings.Login_PhoneBannedError
|
text = presentationData.strings.Login_PhoneBannedError
|
||||||
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
|
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -162,6 +164,7 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
|
|||||||
}))
|
}))
|
||||||
case .generic:
|
case .generic:
|
||||||
text = presentationData.strings.Login_UnknownError
|
text = presentationData.strings.Login_UnknownError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
|
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
|
||||||
|
@ -3,8 +3,6 @@ import UIKit
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
|
|
||||||
private let textFont: UIFont = Font.regular(16.0)
|
|
||||||
|
|
||||||
public final class SolidRoundedButtonTheme {
|
public final class SolidRoundedButtonTheme {
|
||||||
public let backgroundColor: UIColor
|
public let backgroundColor: UIColor
|
||||||
public let foregroundColor: UIColor
|
public let foregroundColor: UIColor
|
||||||
|
@ -241,7 +241,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
|
|
||||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
|
||||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
|
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
|
||||||
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||||
text = foldLineBreaks(text)
|
text = foldLineBreaks(text)
|
||||||
|
|
||||||
@ -288,7 +289,6 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
|
|
||||||
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
|
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
|
||||||
|
|
||||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||||
|
|
||||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
@ -333,7 +333,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 {
|
if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 {
|
||||||
panelData = nil
|
panelData = nil
|
||||||
} else {
|
} else {
|
||||||
panelData = currentGroupCall != nil || availableState?.participantCount == 0 ? nil : availableState
|
panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil) ? nil : availableState
|
||||||
}
|
}
|
||||||
|
|
||||||
let wasEmpty = strongSelf.groupCallPanelData == nil
|
let wasEmpty = strongSelf.groupCallPanelData == nil
|
||||||
@ -406,7 +406,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
strongSelf.joinGroupCall(
|
strongSelf.joinGroupCall(
|
||||||
peerId: groupCallPanelData.peerId,
|
peerId: groupCallPanelData.peerId,
|
||||||
invite: nil,
|
invite: nil,
|
||||||
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title)
|
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribed: false)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if let navigationBar = self.navigationBar {
|
if let navigationBar = self.navigationBar {
|
||||||
|
@ -41,6 +41,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||||||
case accept
|
case accept
|
||||||
case end
|
case end
|
||||||
case cancel
|
case cancel
|
||||||
|
case share
|
||||||
}
|
}
|
||||||
|
|
||||||
var appearance: Appearance
|
var appearance: Appearance
|
||||||
@ -254,6 +255,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||||||
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
|
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
|
||||||
context.strokePath()
|
context.strokePath()
|
||||||
})
|
})
|
||||||
|
case .share:
|
||||||
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: imageColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let image = image {
|
if let image = image {
|
||||||
|
@ -15,11 +15,20 @@ private let blue = UIColor(rgb: 0x0078ff)
|
|||||||
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
||||||
private let green = UIColor(rgb: 0x33c659)
|
private let green = UIColor(rgb: 0x33c659)
|
||||||
private let activeBlue = UIColor(rgb: 0x00a0b9)
|
private let activeBlue = UIColor(rgb: 0x00a0b9)
|
||||||
|
private let purple = UIColor(rgb: 0x3252ef)
|
||||||
|
private let pink = UIColor(rgb: 0xef436c)
|
||||||
|
|
||||||
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||||
|
enum State {
|
||||||
|
case connecting
|
||||||
|
case cantSpeak
|
||||||
|
case active
|
||||||
|
case speaking
|
||||||
|
}
|
||||||
private let foregroundView: UIView
|
private let foregroundView: UIView
|
||||||
private let foregroundGradientLayer: CAGradientLayer
|
private let foregroundGradientLayer: CAGradientLayer
|
||||||
private let maskCurveView: VoiceCurveView
|
private let maskCurveView: VoiceCurveView
|
||||||
|
private let initialTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
var audioLevel: Float = 0.0 {
|
var audioLevel: Float = 0.0 {
|
||||||
didSet {
|
didSet {
|
||||||
@ -35,9 +44,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var speaking: Bool? = nil {
|
var state: State = .connecting {
|
||||||
didSet {
|
didSet {
|
||||||
if self.speaking != oldValue {
|
if self.state != oldValue {
|
||||||
self.updateGradientColors()
|
self.updateGradientColors()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,13 +55,26 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
|||||||
private func updateGradientColors() {
|
private func updateGradientColors() {
|
||||||
let initialColors = self.foregroundGradientLayer.colors
|
let initialColors = self.foregroundGradientLayer.colors
|
||||||
let targetColors: [CGColor]
|
let targetColors: [CGColor]
|
||||||
if let speaking = self.speaking {
|
switch self.state {
|
||||||
targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor]
|
case .connecting:
|
||||||
} else {
|
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
|
||||||
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
|
case .active:
|
||||||
|
targetColors = [blue.cgColor, lightBlue.cgColor]
|
||||||
|
case .speaking:
|
||||||
|
targetColors = [green.cgColor, activeBlue.cgColor]
|
||||||
|
case .cantSpeak:
|
||||||
|
targetColors = [purple.cgColor, pink.cgColor]
|
||||||
|
}
|
||||||
|
|
||||||
|
if CACurrentMediaTime() - self.initialTimestamp > 0.1 {
|
||||||
|
self.foregroundGradientLayer.colors = targetColors
|
||||||
|
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||||
|
} else {
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
self.foregroundGradientLayer.colors = targetColors
|
||||||
|
CATransaction.commit()
|
||||||
}
|
}
|
||||||
self.foregroundGradientLayer.colors = targetColors
|
|
||||||
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let hierarchyTrackingNode: HierarchyTrackingNode
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
@ -177,6 +199,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
private var currentCallState: PresentationCallState?
|
private var currentCallState: PresentationCallState?
|
||||||
private var currentGroupCallState: PresentationGroupCallSummaryState?
|
private var currentGroupCallState: PresentationGroupCallSummaryState?
|
||||||
private var currentIsMuted = true
|
private var currentIsMuted = true
|
||||||
|
private var currentCantSpeak = false
|
||||||
private var currentMembers: PresentationGroupCallMembers?
|
private var currentMembers: PresentationGroupCallMembers?
|
||||||
private var currentIsConnected = true
|
private var currentIsConnected = true
|
||||||
|
|
||||||
@ -279,16 +302,24 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
strongSelf.currentMembers = members
|
strongSelf.currentMembers = members
|
||||||
|
|
||||||
var isMuted = isMuted
|
var isMuted = isMuted
|
||||||
|
var cantSpeak = false
|
||||||
if let state = state, let muteState = state.callState.muteState {
|
if let state = state, let muteState = state.callState.muteState {
|
||||||
if !muteState.canUnmute {
|
if !muteState.canUnmute {
|
||||||
isMuted = true
|
isMuted = true
|
||||||
|
cantSpeak = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if state?.callState.scheduleTimestamp != nil {
|
||||||
|
cantSpeak = true
|
||||||
|
}
|
||||||
strongSelf.currentIsMuted = isMuted
|
strongSelf.currentIsMuted = isMuted
|
||||||
|
strongSelf.currentCantSpeak = cantSpeak
|
||||||
|
|
||||||
let currentIsConnected: Bool
|
let currentIsConnected: Bool
|
||||||
if let state = state, case .connected = state.callState.networkState {
|
if let state = state, case .connected = state.callState.networkState {
|
||||||
currentIsConnected = true
|
currentIsConnected = true
|
||||||
|
} else if state?.callState.scheduleTimestamp != nil {
|
||||||
|
currentIsConnected = true
|
||||||
} else {
|
} else {
|
||||||
currentIsConnected = false
|
currentIsConnected = false
|
||||||
}
|
}
|
||||||
@ -439,7 +470,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize)
|
self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.backgroundNode.speaking = self.currentIsConnected ? !self.currentIsMuted : nil
|
let state: CallStatusBarBackgroundNode.State
|
||||||
|
if self.currentIsConnected {
|
||||||
|
if self.currentCantSpeak {
|
||||||
|
state = .cantSpeak
|
||||||
|
} else if self.currentIsMuted {
|
||||||
|
state = .active
|
||||||
|
} else {
|
||||||
|
state = .speaking
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = .connecting
|
||||||
|
}
|
||||||
|
self.backgroundNode.state = state
|
||||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,29 @@ import SyncCore
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
import TelegramStringFormatting
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AnimatedAvatarSetNode
|
import AnimatedAvatarSetNode
|
||||||
import AudioBlob
|
import AudioBlob
|
||||||
|
|
||||||
|
func textForTimeout(value: Int32) -> String {
|
||||||
|
if value < 3600 {
|
||||||
|
let minutes = value / 60
|
||||||
|
let seconds = value % 60
|
||||||
|
let secondsPadding = seconds < 10 ? "0" : ""
|
||||||
|
return "\(minutes):\(secondsPadding)\(seconds)"
|
||||||
|
} else {
|
||||||
|
let hours = value / 3600
|
||||||
|
let minutes = (value % 3600) / 60
|
||||||
|
let minutesPadding = minutes < 10 ? "0" : ""
|
||||||
|
let seconds = value % 60
|
||||||
|
let secondsPadding = seconds < 10 ? "0" : ""
|
||||||
|
return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let titleFont = Font.semibold(15.0)
|
private let titleFont = Font.semibold(15.0)
|
||||||
private let subtitleFont = Font.regular(13.0)
|
private let subtitleFont = Font.regular(13.0)
|
||||||
|
|
||||||
@ -79,6 +96,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var theme: PresentationTheme
|
private var theme: PresentationTheme
|
||||||
private var strings: PresentationStrings
|
private var strings: PresentationStrings
|
||||||
|
private var dateTimeFormat: PresentationDateTimeFormat
|
||||||
|
|
||||||
private let tapAction: () -> Void
|
private let tapAction: () -> Void
|
||||||
|
|
||||||
@ -102,6 +120,10 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
private var textIsActive = false
|
private var textIsActive = false
|
||||||
private let muteIconNode: ASImageNode
|
private let muteIconNode: ASImageNode
|
||||||
|
|
||||||
|
private var isScheduled = false
|
||||||
|
private var currentText: String = ""
|
||||||
|
private var updateTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
private let avatarsContext: AnimatedAvatarSetContext
|
private let avatarsContext: AnimatedAvatarSetContext
|
||||||
private var avatarsContent: AnimatedAvatarSetContext.Content?
|
private var avatarsContent: AnimatedAvatarSetContext.Content?
|
||||||
private let avatarsNode: AnimatedAvatarSetNode
|
private let avatarsNode: AnimatedAvatarSetNode
|
||||||
@ -125,6 +147,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
self.strings = presentationData.strings
|
self.strings = presentationData.strings
|
||||||
|
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||||
|
|
||||||
self.tapAction = tapAction
|
self.tapAction = tapAction
|
||||||
|
|
||||||
@ -135,6 +158,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
self.joinButton = HighlightableButtonNode()
|
self.joinButton = HighlightableButtonNode()
|
||||||
self.joinButtonTitleNode = ImmediateTextNode()
|
self.joinButtonTitleNode = ImmediateTextNode()
|
||||||
self.joinButtonBackgroundNode = ASImageNode()
|
self.joinButtonBackgroundNode = ASImageNode()
|
||||||
|
self.joinButtonBackgroundNode.clipsToBounds = true
|
||||||
|
self.joinButtonBackgroundNode.displaysAsynchronously = false
|
||||||
|
self.joinButtonBackgroundNode.cornerRadius = 14.0
|
||||||
|
|
||||||
self.micButton = HighlightTrackingButtonNode()
|
self.micButton = HighlightTrackingButtonNode()
|
||||||
self.micButtonForegroundNode = VoiceChatMicrophoneNode()
|
self.micButtonForegroundNode = VoiceChatMicrophoneNode()
|
||||||
@ -198,6 +224,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
self.membersDisposable.dispose()
|
self.membersDisposable.dispose()
|
||||||
self.isMutedDisposable.dispose()
|
self.isMutedDisposable.dispose()
|
||||||
self.audioLevelGeneratorTimer?.invalidate()
|
self.audioLevelGeneratorTimer?.invalidate()
|
||||||
|
self.updateTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func didLoad() {
|
public override func didLoad() {
|
||||||
@ -250,6 +277,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
public func updatePresentationData(_ presentationData: PresentationData) {
|
public func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
self.strings = presentationData.strings
|
self.strings = presentationData.strings
|
||||||
|
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||||
|
|
||||||
self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
|
self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
|
||||||
|
|
||||||
@ -257,18 +285,31 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
|
|
||||||
self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor
|
self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor
|
||||||
|
|
||||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
|
self.joinButtonTitleNode.attributedText = NSAttributedString(string: self.joinButtonTitleNode.attributedText?.string ?? "", font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
|
||||||
self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor)
|
|
||||||
|
|
||||||
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor)
|
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor)
|
||||||
|
|
||||||
self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
|
self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
|
||||||
|
|
||||||
|
self.updateJoinButton()
|
||||||
|
|
||||||
if let (size, leftInset, rightInset) = self.validLayout {
|
if let (size, leftInset, rightInset) = self.validLayout {
|
||||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateJoinButton() {
|
||||||
|
if self.isScheduled {
|
||||||
|
let purple = UIColor(rgb: 0x3252ef)
|
||||||
|
let pink = UIColor(rgb: 0xef436c)
|
||||||
|
self.joinButtonBackgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 1.0), colors: [purple, pink], locations: [0.0, 1.0], direction: .horizontal)
|
||||||
|
self.joinButtonBackgroundNode.backgroundColor = nil
|
||||||
|
} else {
|
||||||
|
self.joinButtonBackgroundNode.image = nil
|
||||||
|
self.joinButtonBackgroundNode.backgroundColor = self.theme.chat.inputPanel.actionControlFillColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func animateTextChange() {
|
private func animateTextChange() {
|
||||||
if let snapshotView = self.textNode.view.snapshotContentTree() {
|
if let snapshotView = self.textNode.view.snapshotContentTree() {
|
||||||
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
|
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
|
||||||
@ -298,6 +339,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
||||||
}
|
}
|
||||||
|
self.currentText = membersText
|
||||||
|
|
||||||
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
||||||
|
|
||||||
@ -321,8 +363,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
|
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
|
||||||
}
|
}
|
||||||
|
strongSelf.currentText = membersText
|
||||||
strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
|
|
||||||
|
|
||||||
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
|
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
|
||||||
|
|
||||||
@ -382,7 +423,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
strongSelf.micButton.view.insertSubview(audioLevelView, at: 0)
|
strongSelf.micButton.view.insertSubview(audioLevelView, at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let level = min(1.0, max(0.0, CGFloat(value)))
|
|
||||||
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
||||||
if value > 0.0 {
|
if value > 0.0 {
|
||||||
strongSelf.audioLevelView?.startAnimating()
|
strongSelf.audioLevelView?.startAnimating()
|
||||||
@ -400,8 +440,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
||||||
}
|
}
|
||||||
|
self.currentText = membersText
|
||||||
self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
|
|
||||||
|
|
||||||
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
||||||
|
|
||||||
@ -466,6 +505,57 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
|
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var joinText = self.strings.VoiceChat_PanelJoin.uppercased()
|
||||||
|
var title = self.strings.VoiceChat_Title
|
||||||
|
var text = self.currentText
|
||||||
|
var isScheduled = false
|
||||||
|
if let scheduleTime = self.currentData?.info.scheduleTimestamp {
|
||||||
|
isScheduled = true
|
||||||
|
let timeString = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime)
|
||||||
|
if let voiceChatTitle = self.currentData?.info.title {
|
||||||
|
title = voiceChatTitle
|
||||||
|
text = self.strings.Conversation_ScheduledVoiceChatStartsOn(timeString).0
|
||||||
|
} else {
|
||||||
|
title = self.strings.Conversation_ScheduledVoiceChat
|
||||||
|
text = self.strings.Conversation_ScheduledVoiceChatStartsOnShort(timeString).0
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||||
|
let elapsedTime = scheduleTime - currentTime
|
||||||
|
if elapsedTime >= 86400 {
|
||||||
|
joinText = timeIntervalString(strings: strings, value: elapsedTime)
|
||||||
|
} else if elapsedTime < 0 {
|
||||||
|
joinText = "+\(textForTimeout(value: abs(elapsedTime)))"
|
||||||
|
} else {
|
||||||
|
joinText = textForTimeout(value: elapsedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.updateTimer == nil {
|
||||||
|
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||||
|
if let strongSelf = self, let (size, leftInset, rightInset) = strongSelf.validLayout {
|
||||||
|
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||||
|
}
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.updateTimer = timer
|
||||||
|
timer.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let timer = self.updateTimer {
|
||||||
|
self.updateTimer = nil
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
|
||||||
|
title = voiceChatTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isScheduled != isScheduled {
|
||||||
|
self.isScheduled = isScheduled
|
||||||
|
self.updateJoinButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.chat.inputPanel.actionControlForegroundColor)
|
||||||
|
|
||||||
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
||||||
let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0)
|
let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0)
|
||||||
let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize)
|
let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize)
|
||||||
@ -501,14 +591,16 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = self.strings.VoiceChat_Title
|
|
||||||
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
|
|
||||||
title = voiceChatTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor)
|
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor)
|
||||||
|
|
||||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width / 2.0 - 56.0, height: .greatestFiniteMagnitude))
|
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
|
||||||
|
|
||||||
|
var constrainedWidth = size.width / 2.0 - 56.0
|
||||||
|
if isScheduled {
|
||||||
|
constrainedWidth = size.width - 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = self.titleNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
|
||||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize)
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize)
|
||||||
|
@ -624,6 +624,113 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
|
||||||
|
let (presentationData, present, openSettings) = self.getDeviceAccessData()
|
||||||
|
|
||||||
|
let isVideo = false
|
||||||
|
|
||||||
|
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
|
||||||
|
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
|
||||||
|
present(c, a)
|
||||||
|
}, openSettings: {
|
||||||
|
openSettings()
|
||||||
|
}, { value in
|
||||||
|
if isVideo && value {
|
||||||
|
DeviceAccess.authorizeAccess(to: .camera(.videoCall), presentationData: presentationData, present: { c, a in
|
||||||
|
present(c, a)
|
||||||
|
}, openSettings: {
|
||||||
|
openSettings()
|
||||||
|
}, { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
subscriber.putNext(value)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|
||||||
|
return accessEnabledSignal
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> mapToSignal { [weak self] accessEnabled -> Signal<Bool, NoError> in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return .single(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !accessEnabled {
|
||||||
|
return .single(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let call = PresentationGroupCallImpl(
|
||||||
|
accountContext: accountContext,
|
||||||
|
audioSession: strongSelf.audioSession,
|
||||||
|
callKitIntegration: nil,
|
||||||
|
getDeviceAccessData: strongSelf.getDeviceAccessData,
|
||||||
|
initialCall: nil,
|
||||||
|
internalId: internalId,
|
||||||
|
peerId: peerId,
|
||||||
|
invite: nil,
|
||||||
|
joinAsPeerId: nil
|
||||||
|
)
|
||||||
|
strongSelf.updateCurrentGroupCall(call)
|
||||||
|
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||||
|
strongSelf.hasActiveGroupCallsPromise.set(true)
|
||||||
|
strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|
||||||
|
|> filter { $0 }
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||||
|
guard let strongSelf = self, let call = call else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if value {
|
||||||
|
if strongSelf.currentGroupCall === call {
|
||||||
|
strongSelf.updateCurrentGroupCall(nil)
|
||||||
|
strongSelf.currentGroupCallPromise.set(.single(nil))
|
||||||
|
strongSelf.hasActiveGroupCallsPromise.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return .single(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult {
|
||||||
|
let begin: () -> Void = { [weak self] in
|
||||||
|
let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentGroupCall = self.currentGroupCallValue {
|
||||||
|
if endCurrentIfAny {
|
||||||
|
let endSignal = currentGroupCall.leave(terminateIfPossible: false)
|
||||||
|
|> filter { $0 }
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
self.startCallDisposable.set(endSignal.start(next: { _ in
|
||||||
|
begin()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return .alreadyInProgress(currentGroupCall.peerId)
|
||||||
|
}
|
||||||
|
} else if let currentCall = self.currentCall {
|
||||||
|
if endCurrentIfAny {
|
||||||
|
self.callKitIntegration?.dropCall(uuid: currentCall.internalId)
|
||||||
|
self.startCallDisposable.set((currentCall.hangUp()
|
||||||
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
|
begin()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return .alreadyInProgress(currentCall.peerId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
begin()
|
||||||
|
}
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
|
public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
|
||||||
let begin: () -> Void = { [weak self] in
|
let begin: () -> Void = { [weak self] in
|
||||||
if let requestJoinAsPeerId = requestJoinAsPeerId {
|
if let requestJoinAsPeerId = requestJoinAsPeerId {
|
||||||
|
@ -77,6 +77,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
|||||||
clientParams: nil,
|
clientParams: nil,
|
||||||
streamDcId: nil,
|
streamDcId: nil,
|
||||||
title: call.title,
|
title: call.title,
|
||||||
|
scheduleTimestamp: call.scheduleTimestamp,
|
||||||
recordingStartTimestamp: nil,
|
recordingStartTimestamp: nil,
|
||||||
sortAscending: true
|
sortAscending: true
|
||||||
),
|
),
|
||||||
@ -120,7 +121,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
|||||||
}
|
}
|
||||||
return GroupCallPanelData(
|
return GroupCallPanelData(
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, recordingStartTimestamp: nil, sortAscending: state.sortAscending),
|
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, scheduleTimestamp: state.scheduleTimestamp, recordingStartTimestamp: nil, sortAscending: state.sortAscending),
|
||||||
topParticipants: topParticipants,
|
topParticipants: topParticipants,
|
||||||
participantCount: state.totalCount,
|
participantCount: state.totalCount,
|
||||||
activeSpeakers: activeSpeakers,
|
activeSpeakers: activeSpeakers,
|
||||||
@ -205,7 +206,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension PresentationGroupCallState {
|
private extension PresentationGroupCallState {
|
||||||
static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState {
|
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?) -> PresentationGroupCallState {
|
||||||
return PresentationGroupCallState(
|
return PresentationGroupCallState(
|
||||||
myPeerId: myPeerId,
|
myPeerId: myPeerId,
|
||||||
networkState: .connecting,
|
networkState: .connecting,
|
||||||
@ -215,7 +216,8 @@ private extension PresentationGroupCallState {
|
|||||||
defaultParticipantMuteState: nil,
|
defaultParticipantMuteState: nil,
|
||||||
recordingStartTimestamp: nil,
|
recordingStartTimestamp: nil,
|
||||||
title: title,
|
title: title,
|
||||||
raisedHand: false
|
raisedHand: false,
|
||||||
|
scheduleTimestamp: scheduleTimestamp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,6 +510,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
private let joinDisposable = MetaDisposable()
|
private let joinDisposable = MetaDisposable()
|
||||||
private let requestDisposable = MetaDisposable()
|
private let requestDisposable = MetaDisposable()
|
||||||
|
private let startDisposable = MetaDisposable()
|
||||||
private var groupCallParticipantUpdatesDisposable: Disposable?
|
private var groupCallParticipantUpdatesDisposable: Disposable?
|
||||||
|
|
||||||
private let networkStateDisposable = MetaDisposable()
|
private let networkStateDisposable = MetaDisposable()
|
||||||
@ -550,6 +553,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
private var peerUpdatesSubscription: Disposable?
|
private var peerUpdatesSubscription: Disposable?
|
||||||
|
|
||||||
|
public private(set) var schedulePending = false
|
||||||
|
|
||||||
init(
|
init(
|
||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
audioSession: ManagedAudioSession,
|
audioSession: ManagedAudioSession,
|
||||||
@ -572,8 +577,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.invite = invite
|
self.invite = invite
|
||||||
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
|
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
|
||||||
|
self.schedulePending = initialCall == nil
|
||||||
|
|
||||||
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title)
|
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp)
|
||||||
self.statePromise = ValuePromise(self.stateValue)
|
self.statePromise = ValuePromise(self.stateValue)
|
||||||
|
|
||||||
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||||
@ -761,7 +767,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
|
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
|
||||||
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title))
|
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title, scheduleTimestamp: initialCall.scheduleTimestamp, subscribed: initialCall.subscribed))
|
||||||
}) {
|
}) {
|
||||||
self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId)
|
self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId)
|
||||||
} else {
|
} else {
|
||||||
@ -805,7 +811,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
strongSelf.stateValue = updatedValue
|
strongSelf.stateValue = updatedValue
|
||||||
})
|
})
|
||||||
|
|
||||||
self.requestCall(movingFromBroadcastToRtc: false)
|
if let _ = self.initialCall {
|
||||||
|
self.requestCall(movingFromBroadcastToRtc: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -815,6 +823,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.audioSessionDisposable?.dispose()
|
self.audioSessionDisposable?.dispose()
|
||||||
self.joinDisposable.dispose()
|
self.joinDisposable.dispose()
|
||||||
self.requestDisposable.dispose()
|
self.requestDisposable.dispose()
|
||||||
|
self.startDisposable.dispose()
|
||||||
self.groupCallParticipantUpdatesDisposable?.dispose()
|
self.groupCallParticipantUpdatesDisposable?.dispose()
|
||||||
self.leaveDisposable.dispose()
|
self.leaveDisposable.dispose()
|
||||||
self.isMutedDisposable.dispose()
|
self.isMutedDisposable.dispose()
|
||||||
@ -1039,287 +1048,301 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldJoin = false
|
||||||
|
let activeCallInfo: GroupCallInfo?
|
||||||
switch previousInternalState {
|
switch previousInternalState {
|
||||||
case .active:
|
case let .active(previousCallInfo):
|
||||||
break
|
if case let .active(callInfo) = internalState {
|
||||||
default:
|
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
|
||||||
if case let .active(callInfo) = internalState {
|
activeCallInfo = callInfo
|
||||||
let callContext: OngoingGroupCallContext
|
|
||||||
if let current = self.callContext {
|
|
||||||
callContext = current
|
|
||||||
} else {
|
} else {
|
||||||
var outgoingAudioBitrateKbit: Int32?
|
activeCallInfo = nil
|
||||||
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
|
}
|
||||||
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 {
|
default:
|
||||||
outgoingAudioBitrateKbit = value
|
if case let .active(callInfo) = internalState {
|
||||||
}
|
shouldJoin = callInfo.scheduleTimestamp == nil
|
||||||
|
activeCallInfo = callInfo
|
||||||
|
} else {
|
||||||
|
activeCallInfo = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
|
if shouldJoin, let callInfo = activeCallInfo {
|
||||||
Queue.mainQueue().async {
|
let callContext: OngoingGroupCallContext
|
||||||
guard let strongSelf = self else {
|
if let current = self.callContext {
|
||||||
return
|
callContext = current
|
||||||
}
|
} else {
|
||||||
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
|
var outgoingAudioBitrateKbit: Int32?
|
||||||
}
|
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
|
||||||
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
|
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 {
|
||||||
Queue.mainQueue().async {
|
outgoingAudioBitrateKbit = value
|
||||||
guard let strongSelf = self else {
|
}
|
||||||
return
|
|
||||||
}
|
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
|
||||||
if case .established = strongSelf.internalState {
|
Queue.mainQueue().async {
|
||||||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
|
|
||||||
self.incomingVideoSourcePromise.set(callContext.videoSources
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|> map { [weak self] sources -> [PeerId: UInt32] in
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return [:]
|
return
|
||||||
}
|
}
|
||||||
var result: [PeerId: UInt32] = [:]
|
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
|
||||||
for source in sources {
|
}
|
||||||
if let peerId = strongSelf.ssrcMapping[source] {
|
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
|
||||||
result[peerId] = source
|
Queue.mainQueue().async {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if case .established = strongSelf.internalState {
|
||||||
|
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
|
||||||
|
self.incomingVideoSourcePromise.set(callContext.videoSources
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> map { [weak self] sources -> [PeerId: UInt32] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
var result: [PeerId: UInt32] = [:]
|
||||||
|
for source in sources {
|
||||||
|
if let peerId = strongSelf.ssrcMapping[source] {
|
||||||
|
result[peerId] = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
self.callContext = callContext
|
||||||
|
}
|
||||||
|
self.joinDisposable.set((callContext.joinPayload
|
||||||
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||||
|
if lhs.0 != rhs.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.1 != rhs.1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerAdminIds: Signal<[PeerId], NoError>
|
||||||
|
let peerId = strongSelf.peerId
|
||||||
|
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
|
peerAdminIds = Signal { subscriber in
|
||||||
|
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in
|
||||||
|
var peerIds = Set<PeerId>()
|
||||||
|
for item in list.list {
|
||||||
|
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
||||||
|
peerIds.insert(item.peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscriber.putNext(Array(peerIds))
|
||||||
|
})
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
|> runOn(.mainQueue())
|
||||||
|
} else {
|
||||||
|
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
||||||
|
var result: [PeerId] = []
|
||||||
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||||
|
if let participants = cachedData.participants {
|
||||||
|
for participant in participants.participants {
|
||||||
|
if case .creator = participant {
|
||||||
|
result.append(participant.peerId)
|
||||||
|
} else if case .admin = participant {
|
||||||
|
result.append(participant.peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
}
|
||||||
self.callContext = callContext
|
|
||||||
}
|
}
|
||||||
self.joinDisposable.set((callContext.joinPayload
|
|
||||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
strongSelf.currentLocalSsrc = ssrc
|
||||||
if lhs.0 != rhs.0 {
|
strongSelf.requestDisposable.set((joinGroupCall(
|
||||||
return false
|
account: strongSelf.account,
|
||||||
}
|
peerId: strongSelf.peerId,
|
||||||
if lhs.1 != rhs.1 {
|
joinAs: strongSelf.joinAsPeerId,
|
||||||
return false
|
callId: callInfo.id,
|
||||||
}
|
accessHash: callInfo.accessHash,
|
||||||
return true
|
preferMuted: true,
|
||||||
})
|
joinPayload: joinPayload,
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
|
peerAdminIds: peerAdminIds,
|
||||||
|
inviteHash: strongSelf.invite
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let clientParams = joinCallResult.callInfo.clientParams {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let peerAdminIds: Signal<[PeerId], NoError>
|
switch joinCallResult.connectionMode {
|
||||||
let peerId = strongSelf.peerId
|
case .rtc:
|
||||||
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
|
strongSelf.currentConnectionMode = .rtc
|
||||||
peerAdminIds = Signal { subscriber in
|
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
||||||
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in
|
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
|
||||||
var peerIds = Set<PeerId>()
|
case .broadcast:
|
||||||
for item in list.list {
|
strongSelf.currentConnectionMode = .broadcast
|
||||||
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
|
||||||
peerIds.insert(item.peer.id)
|
}
|
||||||
}
|
|
||||||
|
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
|
||||||
|
}
|
||||||
|
}, error: { error in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if case .anonymousNotAllowed = error {
|
||||||
|
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
||||||
|
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||||
|
]), on: .root, blockInteraction: false, completion: {})
|
||||||
|
} else if case .tooManyParticipants = error {
|
||||||
|
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
||||||
|
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||||
|
]), on: .root, blockInteraction: false, completion: {})
|
||||||
|
} else if case .invalidJoinAsPeer = error {
|
||||||
|
let peerId = strongSelf.peerId
|
||||||
|
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
|
||||||
|
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||||
|
if let current = current as? CachedChannelData {
|
||||||
|
return current.withUpdatedCallJoinPeerId(nil)
|
||||||
|
} else if let current = current as? CachedGroupData {
|
||||||
|
return current.withUpdatedCallJoinPeerId(nil)
|
||||||
|
} else {
|
||||||
|
return current
|
||||||
}
|
}
|
||||||
subscriber.putNext(Array(peerIds))
|
|
||||||
})
|
})
|
||||||
return disposable
|
}).start()
|
||||||
}
|
|
||||||
|> distinctUntilChanged
|
|
||||||
|> runOn(.mainQueue())
|
|
||||||
} else {
|
|
||||||
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
|
||||||
var result: [PeerId] = []
|
|
||||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
|
||||||
if let participants = cachedData.participants {
|
|
||||||
for participant in participants.participants {
|
|
||||||
if case .creator = participant {
|
|
||||||
result.append(participant.peerId)
|
|
||||||
} else if case .admin = participant {
|
|
||||||
result.append(participant.peerId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
strongSelf.markAsCanBeRemoved()
|
||||||
strongSelf.currentLocalSsrc = ssrc
|
|
||||||
strongSelf.requestDisposable.set((joinGroupCall(
|
|
||||||
account: strongSelf.account,
|
|
||||||
peerId: strongSelf.peerId,
|
|
||||||
joinAs: strongSelf.joinAsPeerId,
|
|
||||||
callId: callInfo.id,
|
|
||||||
accessHash: callInfo.accessHash,
|
|
||||||
preferMuted: true,
|
|
||||||
joinPayload: joinPayload,
|
|
||||||
peerAdminIds: peerAdminIds,
|
|
||||||
inviteHash: strongSelf.invite
|
|
||||||
)
|
|
||||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let clientParams = joinCallResult.callInfo.clientParams {
|
|
||||||
strongSelf.ssrcMapping.removeAll()
|
|
||||||
let addedParticipants: [(UInt32, String?)] = []
|
|
||||||
for participant in joinCallResult.state.participants {
|
|
||||||
if let ssrc = participant.ssrc {
|
|
||||||
strongSelf.ssrcMapping[ssrc] = participant.peer.id
|
|
||||||
//addedParticipants.append((participant.ssrc, participant.jsonParams))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch joinCallResult.connectionMode {
|
|
||||||
case .rtc:
|
|
||||||
strongSelf.currentConnectionMode = .rtc
|
|
||||||
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
|
||||||
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
|
|
||||||
case .broadcast:
|
|
||||||
strongSelf.currentConnectionMode = .broadcast
|
|
||||||
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
|
|
||||||
}
|
|
||||||
}, error: { error in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if case .anonymousNotAllowed = error {
|
|
||||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
|
||||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
|
||||||
]), on: .root, blockInteraction: false, completion: {})
|
|
||||||
} else if case .tooManyParticipants = error {
|
|
||||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
|
||||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
|
||||||
]), on: .root, blockInteraction: false, completion: {})
|
|
||||||
} else if case .invalidJoinAsPeer = error {
|
|
||||||
let peerId = strongSelf.peerId
|
|
||||||
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
|
|
||||||
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
|
|
||||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
|
||||||
if let current = current as? CachedChannelData {
|
|
||||||
return current.withUpdatedCallJoinPeerId(nil)
|
|
||||||
} else if let current = current as? CachedGroupData {
|
|
||||||
return current.withUpdatedCallJoinPeerId(nil)
|
|
||||||
} else {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).start()
|
|
||||||
}
|
|
||||||
strongSelf.markAsCanBeRemoved()
|
|
||||||
}))
|
|
||||||
}))
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
self.networkStateDisposable.set((callContext.networkState
|
self.networkStateDisposable.set((callContext.networkState
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let mappedState: PresentationGroupCallState.NetworkState
|
let mappedState: PresentationGroupCallState.NetworkState
|
||||||
if state.isConnected {
|
if state.isConnected {
|
||||||
mappedState = .connected
|
mappedState = .connected
|
||||||
} else {
|
} else {
|
||||||
mappedState = .connecting
|
mappedState = .connecting
|
||||||
}
|
}
|
||||||
|
|
||||||
let wasConnecting = strongSelf.stateValue.networkState == .connecting
|
let wasConnecting = strongSelf.stateValue.networkState == .connecting
|
||||||
if strongSelf.stateValue.networkState != mappedState {
|
if strongSelf.stateValue.networkState != mappedState {
|
||||||
strongSelf.stateValue.networkState = mappedState
|
strongSelf.stateValue.networkState = mappedState
|
||||||
}
|
}
|
||||||
let isConnecting = mappedState == .connecting
|
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 {
|
if isConnecting {
|
||||||
strongSelf.didStartConnectingOnce = true
|
strongSelf.startCheckingCallIfNeeded()
|
||||||
|
} else {
|
||||||
|
strongSelf.checkCallDisposable?.dispose()
|
||||||
|
strongSelf.checkCallDisposable = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
|
||||||
|
|
||||||
|
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
|
||||||
|
if isConnecting {
|
||||||
|
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
|
||||||
|
strongSelf.toneRenderer = toneRenderer
|
||||||
|
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||||
|
} else {
|
||||||
|
strongSelf.toneRenderer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isConnecting {
|
||||||
|
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 state.isConnected {
|
if let peer = strongSelf.reconnectingAsPeer {
|
||||||
if !strongSelf.didConnectOnce {
|
strongSelf.reconnectingAsPeer = nil
|
||||||
strongSelf.didConnectOnce = true
|
strongSelf.reconnectedAsEventsPipe.putNext(peer)
|
||||||
|
|
||||||
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
|
self.isNoiseSuppressionEnabledDisposable.set((callContext.isNoiseSuppressionEnabled
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
|
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.audioLevelsDisposable.set((callContext.audioLevels
|
self.audioLevelsDisposable.set((callContext.audioLevels
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
var result: [(PeerId, UInt32, Float, Bool)] = []
|
if let peerId = peerId {
|
||||||
var myLevel: Float = 0.0
|
if case .local = ssrcKey {
|
||||||
var myLevelHasVoice: Bool = false
|
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
||||||
var missingSsrcs = Set<UInt32>()
|
myLevel = level
|
||||||
for (ssrcKey, level, hasVoice) in levels {
|
myLevelHasVoice = hasVoice
|
||||||
var peerId: PeerId?
|
|
||||||
let ssrcValue: UInt32
|
|
||||||
switch ssrcKey {
|
|
||||||
case .local:
|
|
||||||
peerId = strongSelf.joinAsPeerId
|
|
||||||
ssrcValue = 0
|
|
||||||
case let .source(ssrc):
|
|
||||||
peerId = strongSelf.ssrcMapping[ssrc]
|
|
||||||
ssrcValue = ssrc
|
|
||||||
}
|
|
||||||
if let peerId = peerId {
|
|
||||||
if case .local = ssrcKey {
|
|
||||||
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
|
||||||
myLevel = level
|
|
||||||
myLevelHasVoice = hasVoice
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result.append((peerId, ssrcValue, level, hasVoice))
|
|
||||||
} else if ssrcValue != 0 {
|
|
||||||
missingSsrcs.insert(ssrcValue)
|
|
||||||
}
|
}
|
||||||
|
result.append((peerId, ssrcValue, level, hasVoice))
|
||||||
|
} else if ssrcValue != 0 {
|
||||||
|
missingSsrcs.insert(ssrcValue)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.speakingParticipantsContext.update(levels: result)
|
strongSelf.speakingParticipantsContext.update(levels: result)
|
||||||
|
|
||||||
let mappedLevel = myLevel * 1.5
|
let mappedLevel = myLevel * 1.5
|
||||||
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
||||||
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
|
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
|
||||||
|
|
||||||
if !missingSsrcs.isEmpty {
|
if !missingSsrcs.isEmpty {
|
||||||
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
|
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch previousInternalState {
|
switch previousInternalState {
|
||||||
@ -1339,6 +1362,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
if self.stateValue.title != initialState.title {
|
if self.stateValue.title != initialState.title {
|
||||||
self.stateValue.title = initialState.title
|
self.stateValue.title = initialState.title
|
||||||
}
|
}
|
||||||
|
if self.stateValue.scheduleTimestamp != initialState.scheduleTimestamp {
|
||||||
|
self.stateValue.scheduleTimestamp = initialState.scheduleTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
let accountContext = self.accountContext
|
let accountContext = self.accountContext
|
||||||
let peerId = self.peerId
|
let peerId = self.peerId
|
||||||
@ -1630,6 +1656,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
|
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
|
||||||
strongSelf.stateValue.title = state.title
|
strongSelf.stateValue.title = state.title
|
||||||
|
strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp
|
||||||
|
|
||||||
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
|
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
|
||||||
id: callInfo.id,
|
id: callInfo.id,
|
||||||
@ -1638,6 +1665,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
clientParams: nil,
|
clientParams: nil,
|
||||||
streamDcId: nil,
|
streamDcId: nil,
|
||||||
title: state.title,
|
title: state.title,
|
||||||
|
scheduleTimestamp: state.scheduleTimestamp,
|
||||||
recordingStartTimestamp: state.recordingStartTimestamp,
|
recordingStartTimestamp: state.recordingStartTimestamp,
|
||||||
sortAscending: state.sortAscending
|
sortAscending: state.sortAscending
|
||||||
))))
|
))))
|
||||||
@ -1887,7 +1915,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
|
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
|
||||||
self.leaving = true
|
self.leaving = true
|
||||||
if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc {
|
if let callInfo = self.internalState.callInfo {
|
||||||
if terminateIfPossible {
|
if terminateIfPossible {
|
||||||
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
@ -1896,7 +1924,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
strongSelf.markAsCanBeRemoved()
|
strongSelf.markAsCanBeRemoved()
|
||||||
}))
|
}))
|
||||||
} else {
|
} else if let localSsrc = self.currentLocalSsrc {
|
||||||
if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl {
|
if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl {
|
||||||
let account = self.account
|
let account = self.account
|
||||||
let id = callInfo.id
|
let id = callInfo.id
|
||||||
@ -1907,6 +1935,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.markAsCanBeRemoved()
|
self.markAsCanBeRemoved()
|
||||||
|
} else {
|
||||||
|
self.markAsCanBeRemoved()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.markAsCanBeRemoved()
|
self.markAsCanBeRemoved()
|
||||||
@ -1957,6 +1987,39 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.callContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
|
self.callContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func schedule(timestamp: Int32) {
|
||||||
|
guard self.schedulePending else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.schedulePending = false
|
||||||
|
self.stateValue.scheduleTimestamp = timestamp
|
||||||
|
|
||||||
|
self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startScheduled() {
|
||||||
|
guard case let .active(callInfo) = self.internalState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stateValue.scheduleTimestamp = nil
|
||||||
|
|
||||||
|
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
public func raiseHand() {
|
public func raiseHand() {
|
||||||
guard let membersValue = self.membersValue else {
|
guard let membersValue = self.membersValue else {
|
||||||
return
|
return
|
||||||
@ -2207,7 +2270,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let value = value {
|
if let value = value {
|
||||||
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title)
|
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: nil, subscribed: false)
|
||||||
|
|
||||||
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
|
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
|
||||||
} else {
|
} else {
|
||||||
@ -2217,7 +2280,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func invitePeer(_ peerId: PeerId) -> Bool {
|
public func invitePeer(_ peerId: PeerId) -> Bool {
|
||||||
guard case let .established(callInfo, _, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else {
|
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2236,11 +2299,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.invitedPeersValue = updatedInvitedPeers
|
self.invitedPeersValue = updatedInvitedPeers
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateTitle(_ title: String){
|
public func updateTitle(_ title: String) {
|
||||||
guard case let .established(callInfo, _, _, _, _) = self.internalState else {
|
guard let callInfo = self.internalState.callInfo else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.stateValue.title = title
|
||||||
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
|
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ private let blobSize = CGSize(width: 190.0, height: 190.0)
|
|||||||
private let smallScale: CGFloat = 0.48
|
private let smallScale: CGFloat = 0.48
|
||||||
private let smallIconScale: CGFloat = 0.69
|
private let smallIconScale: CGFloat = 0.69
|
||||||
|
|
||||||
|
private let buttonHeight: CGFloat = 52.0
|
||||||
|
|
||||||
final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||||
enum State: Equatable {
|
enum State: Equatable {
|
||||||
enum ActiveState: Equatable {
|
enum ActiveState: Equatable {
|
||||||
@ -35,6 +37,14 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
case on
|
case on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ScheduledState: Equatable {
|
||||||
|
case start
|
||||||
|
case subscribe
|
||||||
|
case unsubscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
case button(text: String)
|
||||||
|
case scheduled(state: ScheduledState)
|
||||||
case connecting
|
case connecting
|
||||||
case active(state: ActiveState)
|
case active(state: ActiveState)
|
||||||
}
|
}
|
||||||
@ -53,6 +63,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
private let iconNode: VoiceChatActionButtonIconNode
|
private let iconNode: VoiceChatActionButtonIconNode
|
||||||
private let titleLabel: ImmediateTextNode
|
private let titleLabel: ImmediateTextNode
|
||||||
private let subtitleLabel: ImmediateTextNode
|
private let subtitleLabel: ImmediateTextNode
|
||||||
|
private let buttonTitleLabel: ImmediateTextNode
|
||||||
|
|
||||||
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)?
|
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)?
|
||||||
|
|
||||||
@ -103,7 +114,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case .connecting:
|
case .connecting, .button, .scheduled:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -121,12 +132,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.bottomNode = ASDisplayNode()
|
self.bottomNode = ASDisplayNode()
|
||||||
|
self.bottomNode.isUserInteractionEnabled = false
|
||||||
self.containerNode = ASDisplayNode()
|
self.containerNode = ASDisplayNode()
|
||||||
|
self.containerNode.isUserInteractionEnabled = false
|
||||||
self.backgroundNode = VoiceChatActionButtonBackgroundNode()
|
self.backgroundNode = VoiceChatActionButtonBackgroundNode()
|
||||||
self.iconNode = VoiceChatActionButtonIconNode(isColored: false)
|
self.iconNode = VoiceChatActionButtonIconNode(isColored: false)
|
||||||
|
|
||||||
self.titleLabel = ImmediateTextNode()
|
self.titleLabel = ImmediateTextNode()
|
||||||
self.subtitleLabel = ImmediateTextNode()
|
self.subtitleLabel = ImmediateTextNode()
|
||||||
|
self.buttonTitleLabel = ImmediateTextNode()
|
||||||
|
self.buttonTitleLabel.isUserInteractionEnabled = false
|
||||||
|
self.buttonTitleLabel.alpha = 0.0
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -138,26 +154,38 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
self.containerNode.addSubnode(self.backgroundNode)
|
self.containerNode.addSubnode(self.backgroundNode)
|
||||||
self.containerNode.addSubnode(self.iconNode)
|
self.containerNode.addSubnode(self.iconNode)
|
||||||
|
|
||||||
|
self.containerNode.addSubnode(self.buttonTitleLabel)
|
||||||
|
|
||||||
self.highligthedChanged = { [weak self] pressing in
|
self.highligthedChanged = { [weak self] pressing in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
guard let (_, _, _, _, small, _, _, snap) = strongSelf.currentParams else {
|
guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pressing {
|
if pressing {
|
||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
if case .button = state {
|
||||||
if small {
|
strongSelf.containerNode.layer.removeAnimation(forKey: "opacity")
|
||||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9)
|
strongSelf.containerNode.alpha = 0.4
|
||||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
|
|
||||||
} else {
|
} else {
|
||||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||||
|
if small {
|
||||||
|
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9)
|
||||||
|
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
|
||||||
|
} else {
|
||||||
|
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if !strongSelf.pressing {
|
} else if !strongSelf.pressing {
|
||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
if case .button = state {
|
||||||
if small {
|
strongSelf.containerNode.alpha = 1.0
|
||||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale)
|
strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
|
|
||||||
} else {
|
} else {
|
||||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||||
|
if small {
|
||||||
|
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale)
|
||||||
|
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
|
||||||
|
} else {
|
||||||
|
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,7 +242,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||||
let totalHeight = titleSize.height + subtitleSize.height + 1.0
|
let totalHeight = titleSize.height + subtitleSize.height + 1.0
|
||||||
|
|
||||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 70.0), size: titleSize)
|
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 88.0), size: titleSize)
|
||||||
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
|
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
|
||||||
|
|
||||||
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
@ -232,7 +260,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case .connecting:
|
case .connecting, .button, .scheduled:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +299,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
|
|
||||||
let icon: VoiceChatActionButtonIconAnimationState
|
let icon: VoiceChatActionButtonIconAnimationState
|
||||||
switch state {
|
switch state {
|
||||||
|
case .button:
|
||||||
|
icon = .empty
|
||||||
|
case let .scheduled(state):
|
||||||
|
switch state {
|
||||||
|
case .start:
|
||||||
|
icon = .start
|
||||||
|
case .subscribe:
|
||||||
|
icon = .subscribe
|
||||||
|
case .unsubscribe:
|
||||||
|
icon = .unsubscribe
|
||||||
|
}
|
||||||
case let .active(state):
|
case let .active(state):
|
||||||
switch state {
|
switch state {
|
||||||
case .on:
|
case .on:
|
||||||
@ -290,7 +329,6 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
self.previousIcon = icon
|
self.previousIcon = icon
|
||||||
|
|
||||||
self.iconNode.enqueueState(icon)
|
self.iconNode.enqueueState(icon)
|
||||||
// self.iconNode.update(state: VoiceChatMicrophoneNode.State(muted: iconMuted, filled: true, color: iconColor), animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(snap: Bool, animated: Bool) {
|
func update(snap: Bool, animated: Bool) {
|
||||||
@ -312,8 +350,26 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
|
|
||||||
self.statePromise.set(state)
|
self.statePromise.set(state)
|
||||||
|
|
||||||
|
if let previousState = previousState, case .button = previousState, case .scheduled = state {
|
||||||
|
self.buttonTitleLabel.alpha = 0.0
|
||||||
|
self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24)
|
||||||
|
|
||||||
|
self.iconNode.alpha = 1.0
|
||||||
|
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||||
|
}
|
||||||
|
|
||||||
var backgroundState: VoiceChatActionButtonBackgroundNode.State
|
var backgroundState: VoiceChatActionButtonBackgroundNode.State
|
||||||
switch state {
|
switch state {
|
||||||
|
case let .button(text):
|
||||||
|
backgroundState = .button
|
||||||
|
self.buttonTitleLabel.alpha = 1.0
|
||||||
|
self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white)
|
||||||
|
let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0))
|
||||||
|
self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
case .scheduled:
|
||||||
|
backgroundState = .disabled
|
||||||
case let .active(state):
|
case let .active(state):
|
||||||
switch state {
|
switch state {
|
||||||
case .on:
|
case .on:
|
||||||
@ -340,14 +396,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
applyParams(animated: animated)
|
self.applyParams(animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
var hitRect = self.bounds
|
var hitRect = self.bounds
|
||||||
if let (_, buttonSize, _, _, _, _, _, _) = self.currentParams {
|
if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams {
|
||||||
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
|
if case .button = state {
|
||||||
|
hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight)
|
||||||
|
} else {
|
||||||
|
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let result = super.hitTest(point, with: event)
|
let result = super.hitTest(point, with: event)
|
||||||
if !hitRect.contains(point) {
|
if !hitRect.contains(point) {
|
||||||
@ -453,6 +513,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
enum State: Equatable {
|
enum State: Equatable {
|
||||||
case connecting
|
case connecting
|
||||||
case disabled
|
case disabled
|
||||||
|
case button
|
||||||
case blob(Bool)
|
case blob(Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,7 +607,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
self.maskProgressLayer.lineCap = .round
|
self.maskProgressLayer.lineCap = .round
|
||||||
self.maskProgressLayer.path = path
|
self.maskProgressLayer.path = path
|
||||||
|
|
||||||
let largerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width + progressLineWidth, height: buttonSize.height + progressLineWidth))).cgPath
|
let circleFrame = CGRect(origin: CGPoint(x: (358 - buttonSize.width) / 2.0, y: (358 - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||||
|
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
|
||||||
|
|
||||||
self.maskCircleLayer.fillColor = white.cgColor
|
self.maskCircleLayer.fillColor = white.cgColor
|
||||||
self.maskCircleLayer.path = largerCirclePath
|
self.maskCircleLayer.path = largerCirclePath
|
||||||
self.maskCircleLayer.isHidden = true
|
self.maskCircleLayer.isHidden = true
|
||||||
@ -872,7 +935,8 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.updateGlowAndGradientAnimations(type: type, previousType: nil)
|
self.updateGlowAndGradientAnimations(type: type, previousType: nil)
|
||||||
|
|
||||||
if case .blob = self.state {
|
if case .connecting = self.state {
|
||||||
|
} else {
|
||||||
self.maskBlobView.isHidden = false
|
self.maskBlobView.isHidden = false
|
||||||
self.maskBlobView.startAnimating()
|
self.maskBlobView.startAnimating()
|
||||||
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||||
@ -907,6 +971,47 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupButtonAnimation() {
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
self.backgroundCircleLayer.isHidden = true
|
||||||
|
self.foregroundCircleLayer.isHidden = true
|
||||||
|
self.maskCircleLayer.isHidden = false
|
||||||
|
self.maskProgressLayer.isHidden = true
|
||||||
|
self.maskGradientLayer.isHidden = true
|
||||||
|
|
||||||
|
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight), cornerRadius: 10.0).cgPath
|
||||||
|
self.maskCircleLayer.path = path
|
||||||
|
|
||||||
|
CATransaction.commit()
|
||||||
|
|
||||||
|
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil)
|
||||||
|
|
||||||
|
self.updatedActive?(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func playScheduledAnimation() {
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
self.maskGradientLayer.isHidden = false
|
||||||
|
CATransaction.commit()
|
||||||
|
|
||||||
|
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||||
|
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
|
||||||
|
|
||||||
|
let previousPath = self.maskCircleLayer.path
|
||||||
|
self.maskCircleLayer.path = largerCirclePath
|
||||||
|
|
||||||
|
self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.42, initialVelocity: 0.0, damping: 104.0)
|
||||||
|
|
||||||
|
self.maskBlobView.isHidden = false
|
||||||
|
self.maskBlobView.startAnimating()
|
||||||
|
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||||
|
|
||||||
|
let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? 0.8)
|
||||||
|
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||||
|
}
|
||||||
|
|
||||||
var isActive = false
|
var isActive = false
|
||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
if !self.isCurrentlyInHierarchy {
|
if !self.isCurrentlyInHierarchy {
|
||||||
@ -959,7 +1064,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
self.isActive = false
|
self.isActive = false
|
||||||
|
|
||||||
if let transition = self.transition {
|
if let transition = self.transition {
|
||||||
if case .connecting = transition {
|
if case .button = transition {
|
||||||
|
self.playScheduledAnimation()
|
||||||
|
} else if case .connecting = transition {
|
||||||
self.playConnectionAnimation(type: .muted) { [weak self] in
|
self.playConnectionAnimation(type: .muted) { [weak self] in
|
||||||
self?.isActive = false
|
self?.isActive = false
|
||||||
}
|
}
|
||||||
@ -969,7 +1076,10 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
self.transition = nil
|
self.transition = nil
|
||||||
}
|
}
|
||||||
break
|
case .button:
|
||||||
|
self.updatedActive?(true)
|
||||||
|
self.isActive = false
|
||||||
|
self.setupButtonAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1037,20 +1147,24 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
override func layout() {
|
override func layout() {
|
||||||
super.layout()
|
super.layout()
|
||||||
|
|
||||||
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height)
|
||||||
|
let center = bounds.center
|
||||||
|
|
||||||
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize)
|
self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize)
|
||||||
|
|
||||||
|
let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize)
|
||||||
self.backgroundCircleLayer.frame = circleFrame
|
self.backgroundCircleLayer.frame = circleFrame
|
||||||
self.foregroundCircleLayer.position = center
|
self.foregroundCircleLayer.position = center
|
||||||
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
|
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
|
||||||
self.growingForegroundCircleLayer.position = center
|
self.growingForegroundCircleLayer.position = center
|
||||||
self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds
|
self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds
|
||||||
self.maskCircleLayer.frame = circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
self.maskCircleLayer.frame = self.bounds
|
||||||
|
// circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||||
self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0)
|
self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0)
|
||||||
self.foregroundView.frame = self.bounds
|
self.foregroundView.frame = self.bounds
|
||||||
self.foregroundGradientLayer.frame = self.bounds
|
self.foregroundGradientLayer.frame = self.bounds
|
||||||
self.maskGradientLayer.position = center
|
self.maskGradientLayer.position = center
|
||||||
self.maskGradientLayer.bounds = self.bounds
|
self.maskGradientLayer.bounds = bounds
|
||||||
self.maskView.frame = self.bounds
|
self.maskView.frame = self.bounds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1386,6 +1500,10 @@ final class BlobView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum VoiceChatActionButtonIconAnimationState: Equatable {
|
enum VoiceChatActionButtonIconAnimationState: Equatable {
|
||||||
|
case empty
|
||||||
|
case start
|
||||||
|
case subscribe
|
||||||
|
case unsubscribe
|
||||||
case unmute
|
case unmute
|
||||||
case mute
|
case mute
|
||||||
case hand
|
case hand
|
||||||
@ -1399,6 +1517,7 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
|||||||
self.isColored = isColored
|
self.isColored = isColored
|
||||||
super.init(size: CGSize(width: 100.0, height: 100.0))
|
super.init(size: CGSize(width: 100.0, height: 100.0))
|
||||||
|
|
||||||
|
self.scale = 0.8
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1410,30 +1529,73 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
|||||||
let previousState = self.iconState
|
let previousState = self.iconState
|
||||||
self.iconState = state
|
self.iconState = state
|
||||||
|
|
||||||
|
if state != .empty {
|
||||||
|
self.alpha = 1.0
|
||||||
|
}
|
||||||
switch previousState {
|
switch previousState {
|
||||||
|
case .empty:
|
||||||
|
switch state {
|
||||||
|
case .start:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case .subscribe:
|
||||||
|
switch state {
|
||||||
|
case .unsubscribe:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
case .mute:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
case .hand:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case .unsubscribe:
|
||||||
|
switch state {
|
||||||
|
case .subscribe:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
case .mute:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
case .hand:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case .start:
|
||||||
|
switch state {
|
||||||
|
case .mute:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
case .unmute:
|
case .unmute:
|
||||||
switch state {
|
switch state {
|
||||||
case .mute:
|
case .mute:
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute")))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute")))
|
||||||
case .hand:
|
case .hand:
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2")))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2")))
|
||||||
case .unmute:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case .mute:
|
case .mute:
|
||||||
switch state {
|
switch state {
|
||||||
|
case .start:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
|
||||||
case .unmute:
|
case .unmute:
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 12), duration: 0.2))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute")))
|
||||||
case .hand:
|
case .hand:
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff")))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff")))
|
||||||
case .mute:
|
case .empty:
|
||||||
|
self.alpha = 0.0
|
||||||
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case .hand:
|
case .hand:
|
||||||
switch state {
|
switch state {
|
||||||
case .mute, .unmute:
|
case .mute, .unmute:
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn")))
|
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn")))
|
||||||
case .hand:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import AsyncDisplayKit
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
import TelegramStringFormatting
|
||||||
import TelegramVoip
|
import TelegramVoip
|
||||||
import TelegramAudio
|
import TelegramAudio
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@ -29,6 +30,7 @@ import LegacyComponents
|
|||||||
import LegacyMediaPickerUI
|
import LegacyMediaPickerUI
|
||||||
import WebSearchUI
|
import WebSearchUI
|
||||||
import MapResourceToAvatarSizes
|
import MapResourceToAvatarSizes
|
||||||
|
import SolidRoundedButtonNode
|
||||||
|
|
||||||
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
|
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||||
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
|
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
|
||||||
@ -65,105 +67,6 @@ private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
|
|||||||
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
|
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private final class VoiceChatControllerTitleNode: ASDisplayNode {
|
|
||||||
private var theme: PresentationTheme
|
|
||||||
|
|
||||||
private let titleNode: ASTextNode
|
|
||||||
private let infoNode: ASTextNode
|
|
||||||
fileprivate let recordingIconNode: VoiceChatRecordingIconNode
|
|
||||||
|
|
||||||
public var isRecording: Bool = false {
|
|
||||||
didSet {
|
|
||||||
self.recordingIconNode.isHidden = !self.isRecording
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tapped: (() -> Void)?
|
|
||||||
|
|
||||||
init(theme: PresentationTheme) {
|
|
||||||
self.theme = theme
|
|
||||||
|
|
||||||
self.titleNode = ASTextNode()
|
|
||||||
self.titleNode.displaysAsynchronously = false
|
|
||||||
self.titleNode.maximumNumberOfLines = 1
|
|
||||||
self.titleNode.truncationMode = .byTruncatingTail
|
|
||||||
self.titleNode.isOpaque = false
|
|
||||||
|
|
||||||
self.infoNode = ASTextNode()
|
|
||||||
self.infoNode.displaysAsynchronously = false
|
|
||||||
self.infoNode.maximumNumberOfLines = 1
|
|
||||||
self.infoNode.truncationMode = .byTruncatingTail
|
|
||||||
self.infoNode.isOpaque = false
|
|
||||||
|
|
||||||
self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false)
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.addSubnode(self.infoNode)
|
|
||||||
self.addSubnode(self.recordingIconNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
|
|
||||||
}
|
|
||||||
|
|
||||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
||||||
if point.y > 0.0 && point.y < self.frame.size.height && point.x > min(self.titleNode.frame.minX, self.infoNode.frame.minX) && point.x < max(self.recordingIconNode.frame.maxX, self.infoNode.frame.maxX) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func tap() {
|
|
||||||
self.tapped?()
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) {
|
|
||||||
var titleUpdated = false
|
|
||||||
if let previousTitle = self.titleNode.attributedText?.string {
|
|
||||||
titleUpdated = previousTitle != title
|
|
||||||
}
|
|
||||||
|
|
||||||
if titleUpdated, let snapshotView = self.titleNode.view.snapshotContentTree() {
|
|
||||||
snapshotView.frame = self.titleNode.frame
|
|
||||||
self.view.addSubview(snapshotView)
|
|
||||||
|
|
||||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
||||||
snapshotView?.removeFromSuperview()
|
|
||||||
})
|
|
||||||
|
|
||||||
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: UIColor(rgb: 0xffffff))
|
|
||||||
self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.5))
|
|
||||||
|
|
||||||
let constrainedSize = CGSize(width: size.width - 140.0, height: size.height)
|
|
||||||
let titleSize = self.titleNode.measure(constrainedSize)
|
|
||||||
let infoSize = self.infoNode.measure(constrainedSize)
|
|
||||||
let titleInfoSpacing: CGFloat = 0.0
|
|
||||||
|
|
||||||
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing
|
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
|
||||||
self.titleNode.frame = titleFrame
|
|
||||||
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize)
|
|
||||||
|
|
||||||
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
|
||||||
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
|
|
||||||
self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class GroupVideoNode: ASDisplayNode {
|
final class GroupVideoNode: ASDisplayNode {
|
||||||
private let videoViewContainer: UIView
|
private let videoViewContainer: UIView
|
||||||
private let videoView: PresentationCallVideoView
|
private let videoView: PresentationCallVideoView
|
||||||
@ -730,7 +633,15 @@ public final class VoiceChatController: ViewController {
|
|||||||
private let leftBorderNode: ASDisplayNode
|
private let leftBorderNode: ASDisplayNode
|
||||||
private let rightBorderNode: ASDisplayNode
|
private let rightBorderNode: ASDisplayNode
|
||||||
|
|
||||||
private let titleNode: VoiceChatControllerTitleNode
|
private var isScheduling = false
|
||||||
|
private let timerNode: VoiceChatTimerNode
|
||||||
|
private var pickerView: UIDatePicker?
|
||||||
|
private let dateFormatter: DateFormatter
|
||||||
|
private let scheduleTextNode: ImmediateTextNode
|
||||||
|
private let scheduleCancelButton: SolidRoundedButtonNode
|
||||||
|
private var scheduleButtonTitle = ""
|
||||||
|
|
||||||
|
private let titleNode: VoiceChatTitleNode
|
||||||
|
|
||||||
private var enqueuedTransitions: [ListTransition] = []
|
private var enqueuedTransitions: [ListTransition] = []
|
||||||
private var floatingHeaderOffset: CGFloat?
|
private var floatingHeaderOffset: CGFloat?
|
||||||
@ -823,6 +734,8 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.context = call.accountContext
|
self.context = call.accountContext
|
||||||
self.call = call
|
self.call = call
|
||||||
|
|
||||||
|
self.isScheduling = call.schedulePending
|
||||||
|
|
||||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
|
|
||||||
@ -836,7 +749,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.contentContainer.isHidden = true
|
self.contentContainer.isHidden = true
|
||||||
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
|
self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor
|
||||||
self.backgroundNode.clipsToBounds = false
|
self.backgroundNode.clipsToBounds = false
|
||||||
|
|
||||||
if sharedContext.immediateExperimentalUISettings.demoVideoChats {
|
if sharedContext.immediateExperimentalUISettings.demoVideoChats {
|
||||||
@ -844,6 +757,8 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.listNode = ListView()
|
self.listNode = ListView()
|
||||||
|
self.listNode.alpha = self.isScheduling ? 0.0 : 1.0
|
||||||
|
self.listNode.isUserInteractionEnabled = !self.isScheduling
|
||||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
||||||
self.listNode.clipsToBounds = true
|
self.listNode.clipsToBounds = true
|
||||||
self.listNode.scroller.bounces = false
|
self.listNode.scroller.bounces = false
|
||||||
@ -870,7 +785,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.closeButton = VoiceChatHeaderButton(context: self.context)
|
self.closeButton = VoiceChatHeaderButton(context: self.context)
|
||||||
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
|
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
|
||||||
|
|
||||||
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
|
self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme)
|
||||||
|
|
||||||
self.topCornersNode = ASImageNode()
|
self.topCornersNode = ASImageNode()
|
||||||
self.topCornersNode.displaysAsynchronously = false
|
self.topCornersNode.displaysAsynchronously = false
|
||||||
@ -896,6 +811,13 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.leaveButton = CallControllerButtonItemNode()
|
self.leaveButton = CallControllerButtonItemNode()
|
||||||
self.actionButton = VoiceChatActionButton()
|
self.actionButton = VoiceChatActionButton()
|
||||||
|
|
||||||
|
if self.isScheduling {
|
||||||
|
self.audioButton.alpha = 0.0
|
||||||
|
self.audioButton.isUserInteractionEnabled = false
|
||||||
|
self.leaveButton.alpha = 0.0
|
||||||
|
self.leaveButton.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
self.leftBorderNode = ASDisplayNode()
|
self.leftBorderNode = ASDisplayNode()
|
||||||
self.leftBorderNode.backgroundColor = panelBackgroundColor
|
self.leftBorderNode.backgroundColor = panelBackgroundColor
|
||||||
self.leftBorderNode.isUserInteractionEnabled = false
|
self.leftBorderNode.isUserInteractionEnabled = false
|
||||||
@ -906,6 +828,19 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.rightBorderNode.isUserInteractionEnabled = false
|
self.rightBorderNode.isUserInteractionEnabled = false
|
||||||
self.rightBorderNode.clipsToBounds = false
|
self.rightBorderNode.clipsToBounds = false
|
||||||
|
|
||||||
|
self.scheduleTextNode = ImmediateTextNode()
|
||||||
|
self.scheduleTextNode.isHidden = !self.isScheduling
|
||||||
|
|
||||||
|
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
|
||||||
|
self.scheduleCancelButton.isHidden = !self.isScheduling
|
||||||
|
|
||||||
|
self.dateFormatter = DateFormatter()
|
||||||
|
self.dateFormatter.timeStyle = .none
|
||||||
|
self.dateFormatter.dateStyle = .short
|
||||||
|
self.dateFormatter.timeZone = TimeZone.current
|
||||||
|
|
||||||
|
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
let statePromise = ValuePromise(State(), ignoreRepeated: true)
|
let statePromise = ValuePromise(State(), ignoreRepeated: true)
|
||||||
@ -1514,6 +1449,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture)
|
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture)
|
||||||
|
contextController.useComplexItemsTransitionAnimation = true
|
||||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||||
}, setPeerIdWithRevealedOptions: { peerId, _ in
|
}, setPeerIdWithRevealedOptions: { peerId, _ in
|
||||||
updateState { state in
|
updateState { state in
|
||||||
@ -1550,6 +1486,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
self.bottomPanelNode.addSubnode(self.leaveButton)
|
self.bottomPanelNode.addSubnode(self.leaveButton)
|
||||||
self.bottomPanelNode.addSubnode(self.actionButton)
|
self.bottomPanelNode.addSubnode(self.actionButton)
|
||||||
|
self.bottomPanelNode.addSubnode(self.scheduleCancelButton)
|
||||||
|
|
||||||
self.addSubnode(self.dimNode)
|
self.addSubnode(self.dimNode)
|
||||||
self.addSubnode(self.contentContainer)
|
self.addSubnode(self.contentContainer)
|
||||||
@ -1563,6 +1500,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.contentContainer.addSubnode(self.leftBorderNode)
|
self.contentContainer.addSubnode(self.leftBorderNode)
|
||||||
self.contentContainer.addSubnode(self.rightBorderNode)
|
self.contentContainer.addSubnode(self.rightBorderNode)
|
||||||
self.contentContainer.addSubnode(self.bottomPanelNode)
|
self.contentContainer.addSubnode(self.bottomPanelNode)
|
||||||
|
self.contentContainer.addSubnode(self.timerNode)
|
||||||
|
|
||||||
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|
||||||
|> mapToSignal { ids -> Signal<[Peer], NoError> in
|
|> mapToSignal { ids -> Signal<[Peer], NoError> in
|
||||||
@ -1619,7 +1557,13 @@ public final class VoiceChatController: ViewController {
|
|||||||
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
|
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
|
||||||
strongSelf.currentSubtitle = subtitle
|
strongSelf.currentSubtitle = subtitle
|
||||||
|
|
||||||
if let callState = strongSelf.callState, callState.canManageCall {
|
if strongSelf.isScheduling {
|
||||||
|
strongSelf.optionsButtonIsAvatar = false
|
||||||
|
strongSelf.optionsButton.isUserInteractionEnabled = false
|
||||||
|
strongSelf.optionsButton.alpha = 0.0
|
||||||
|
strongSelf.closeButton.isUserInteractionEnabled = false
|
||||||
|
strongSelf.closeButton.alpha = 0.0
|
||||||
|
} else if let callState = strongSelf.callState, callState.canManageCall {
|
||||||
strongSelf.optionsButtonIsAvatar = false
|
strongSelf.optionsButtonIsAvatar = false
|
||||||
strongSelf.optionsButton.isUserInteractionEnabled = true
|
strongSelf.optionsButton.isUserInteractionEnabled = true
|
||||||
strongSelf.optionsButton.alpha = 1.0
|
strongSelf.optionsButton.alpha = 1.0
|
||||||
@ -1774,16 +1718,6 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// self.memberEventsDisposable.set((self.call.memberEvents
|
|
||||||
// |> deliverOnMainQueue).start(next: { [weak self] event in
|
|
||||||
// guard let strongSelf = self else {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if event.joined {
|
|
||||||
// strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
|
|
||||||
// }
|
|
||||||
// }))
|
|
||||||
|
|
||||||
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1874,22 +1808,32 @@ public final class VoiceChatController: ViewController {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
self.titleNode.tapped = { [weak self] in
|
self.titleNode.tapped = { [weak self] in
|
||||||
if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden {
|
if let strongSelf = self {
|
||||||
var hasTooltipAlready = false
|
if strongSelf.callState?.canManageCall ?? false {
|
||||||
strongSelf.controller?.forEachController { controller -> Bool in
|
strongSelf.openTitleEditing()
|
||||||
if controller is TooltipScreen {
|
} else if !strongSelf.titleNode.recordingIconNode.isHidden {
|
||||||
hasTooltipAlready = true
|
var hasTooltipAlready = false
|
||||||
|
strongSelf.controller?.forEachController { controller -> Bool in
|
||||||
|
if controller is TooltipScreen {
|
||||||
|
hasTooltipAlready = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !hasTooltipAlready {
|
||||||
|
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
|
||||||
|
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||||
|
return .dismiss(consume: true)
|
||||||
|
}), in: .window(.root))
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !hasTooltipAlready {
|
|
||||||
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
|
|
||||||
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
|
||||||
return .dismiss(consume: true)
|
|
||||||
}), in: .window(.root))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.scheduleCancelButton.pressed = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.dismissScheduled()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -1931,7 +1875,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||||
|
|
||||||
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get())
|
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get())
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
|
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
|
||||||
@ -1965,15 +1909,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
strongSelf.openTitleEditing()
|
||||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
|
|
||||||
if let strongSelf = self, let title = title {
|
|
||||||
strongSelf.call.updateTitle(title)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self?.controller?.present(controller, in: .window(.root))
|
|
||||||
})))
|
})))
|
||||||
|
|
||||||
var hasPermissions = true
|
var hasPermissions = true
|
||||||
@ -1995,15 +1931,6 @@ public final class VoiceChatController: ViewController {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { c, _ in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.setItems(strongSelf.contextMenuPermissionItems())
|
|
||||||
})))
|
|
||||||
|
|
||||||
if let inviteLinks = inviteLinks {
|
if let inviteLinks = inviteLinks {
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||||
@ -2044,25 +1971,27 @@ public final class VoiceChatController: ViewController {
|
|||||||
self?.controller?.present(alertController, in: .window(.root))
|
self?.controller?.present(alertController, in: .window(.root))
|
||||||
}), false))
|
}), false))
|
||||||
} else {
|
} else {
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
if strongSelf.callState?.scheduleTimestamp == nil {
|
||||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
||||||
}, action: { _, f in
|
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||||
f(.dismissWithoutContent)
|
}, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
|
|
||||||
if let strongSelf = self, let title = title {
|
|
||||||
strongSelf.call.setShouldBeRecording(true, title: title)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
|
||||||
strongSelf.call.playTone(.recordingStarted)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
self?.controller?.present(controller, in: .window(.root))
|
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
|
||||||
})))
|
if let strongSelf = self, let title = title {
|
||||||
|
strongSelf.call.setShouldBeRecording(true, title: title)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
||||||
|
strongSelf.call.playTone(.recordingStarted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self?.controller?.present(controller, in: .window(.root))
|
||||||
|
})))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
|
||||||
@ -2275,6 +2204,161 @@ public final class VoiceChatController: ViewController {
|
|||||||
panRecognizer.delaysTouchesBegan = false
|
panRecognizer.delaysTouchesBegan = false
|
||||||
panRecognizer.cancelsTouchesInView = true
|
panRecognizer.cancelsTouchesInView = true
|
||||||
self.view.addGestureRecognizer(panRecognizer)
|
self.view.addGestureRecognizer(panRecognizer)
|
||||||
|
|
||||||
|
if self.isScheduling {
|
||||||
|
self.setupPickerView()
|
||||||
|
self.updateScheduleButtonTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateMinimumDate() {
|
||||||
|
let timeZone = TimeZone(secondsFromGMT: 0)!
|
||||||
|
var calendar = Calendar(identifier: .gregorian)
|
||||||
|
calendar.timeZone = timeZone
|
||||||
|
let currentDate = Date()
|
||||||
|
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
|
||||||
|
components.second = 0
|
||||||
|
let minute = (components.minute ?? 0) % 5
|
||||||
|
|
||||||
|
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!)
|
||||||
|
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!)
|
||||||
|
|
||||||
|
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
|
||||||
|
self.pickerView?.maximumDate = date
|
||||||
|
}
|
||||||
|
|
||||||
|
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate {
|
||||||
|
self.pickerView?.minimumDate = next1MinDate
|
||||||
|
self.pickerView?.date = next5MinDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupPickerView() {
|
||||||
|
var currentDate: Date?
|
||||||
|
if let pickerView = self.pickerView {
|
||||||
|
currentDate = pickerView.date
|
||||||
|
pickerView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let textColor = UIColor.white
|
||||||
|
UILabel.setDateLabel(textColor)
|
||||||
|
|
||||||
|
let pickerView = UIDatePicker()
|
||||||
|
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
|
pickerView.datePickerMode = .countDownTimer
|
||||||
|
pickerView.datePickerMode = .dateAndTime
|
||||||
|
pickerView.locale = Locale.current
|
||||||
|
pickerView.timeZone = TimeZone.current
|
||||||
|
pickerView.minuteInterval = 1
|
||||||
|
self.contentContainer.view.addSubview(pickerView)
|
||||||
|
pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
|
pickerView.preferredDatePickerStyle = .wheels
|
||||||
|
}
|
||||||
|
pickerView.setValue(textColor, forKey: "textColor")
|
||||||
|
self.pickerView = pickerView
|
||||||
|
|
||||||
|
self.updateMinimumDate()
|
||||||
|
if let currentDate = currentDate {
|
||||||
|
pickerView.date = currentDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let calendar = Calendar(identifier: .gregorian)
|
||||||
|
private func updateScheduleButtonTitle() {
|
||||||
|
guard let date = self.pickerView?.date else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let calendar = Calendar(identifier: .gregorian)
|
||||||
|
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||||
|
let buttonTitle: String
|
||||||
|
if calendar.isDateInToday(date) {
|
||||||
|
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
|
||||||
|
} else if calendar.isDateInTomorrow(date) {
|
||||||
|
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).0
|
||||||
|
} else {
|
||||||
|
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).0
|
||||||
|
}
|
||||||
|
self.scheduleButtonTitle = buttonTitle
|
||||||
|
|
||||||
|
if let (layout, navigationHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func datePickerUpdated() {
|
||||||
|
self.updateScheduleButtonTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func schedule() {
|
||||||
|
if let date = self.pickerView?.date, date > Date() {
|
||||||
|
self.call.schedule(timestamp: Int32(date.timeIntervalSince1970))
|
||||||
|
|
||||||
|
self.isScheduling = false
|
||||||
|
self.transitionToScheduled()
|
||||||
|
if let (layout, navigationHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissScheduled() {
|
||||||
|
self.leaveDisposable.set((self.call.leave(terminateIfPossible: true)
|
||||||
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
|
self?.controller?.dismiss(closing: true)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func transitionToScheduled() {
|
||||||
|
self.optionsButton.alpha = 1.0
|
||||||
|
self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||||
|
self.optionsButton.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
self.closeButton.alpha = 1.0
|
||||||
|
self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||||
|
self.closeButton.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
self.audioButton.alpha = 1.0
|
||||||
|
self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||||
|
self.audioButton.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
self.leaveButton.alpha = 1.0
|
||||||
|
self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||||
|
self.leaveButton.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
self.scheduleCancelButton.alpha = 0.0
|
||||||
|
self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||||
|
|
||||||
|
if let pickerView = self.pickerView {
|
||||||
|
pickerView.alpha = 0.0
|
||||||
|
pickerView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||||
|
pickerView.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timerNode.alpha = 1.0
|
||||||
|
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.timerNode.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.3, damping: 104.0)
|
||||||
|
self.timerNode.animateIn()
|
||||||
|
|
||||||
|
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func transitionToCall() {
|
||||||
|
self.updateIsFullscreen(false, force: true)
|
||||||
|
|
||||||
|
self.listNode.alpha = 1.0
|
||||||
|
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.timerNode.alpha = 0.0
|
||||||
|
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
|
||||||
|
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func optionsPressed() {
|
@objc private func optionsPressed() {
|
||||||
@ -2491,7 +2575,31 @@ public final class VoiceChatController: ViewController {
|
|||||||
guard let callState = self.callState else {
|
guard let callState = self.callState else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if case .connecting = callState.networkState {
|
if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if callState.scheduleTimestamp != nil || self.isScheduling {
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began:
|
||||||
|
self.actionButton.pressing = true
|
||||||
|
self.hapticFeedback.impact(.light)
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.actionButton.pressing = false
|
||||||
|
|
||||||
|
let location = gestureRecognizer.location(in: self.actionButton.view)
|
||||||
|
if self.actionButton.hitTest(location, with: nil) != nil {
|
||||||
|
if self.isScheduling {
|
||||||
|
self.schedule()
|
||||||
|
} else if callState.canManageCall {
|
||||||
|
self.call.startScheduled()
|
||||||
|
self.transitionToCall()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let muteState = callState.muteState {
|
if let muteState = callState.muteState {
|
||||||
@ -2548,11 +2656,27 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func actionButtonPressed() {
|
@objc private func actionButtonPressed() {
|
||||||
|
if self.isScheduling {
|
||||||
|
self.schedule()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func audioOutputPressed() {
|
@objc private func audioOutputPressed() {
|
||||||
self.hapticFeedback.impact(.light)
|
self.hapticFeedback.impact(.light)
|
||||||
|
|
||||||
|
if let _ = self.callState?.scheduleTimestamp {
|
||||||
|
let _ = (self.inviteLinksPromise.get()
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
|
||||||
|
if let inviteLinks = inviteLinks {
|
||||||
|
self?.presentShare(inviteLinks)
|
||||||
|
} else {
|
||||||
|
self?.presentShare(GroupCallInviteLinks(listenerLink: "a", speakerLink: nil))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2743,8 +2867,8 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isFullscreen = false
|
var isFullscreen = false
|
||||||
func updateIsFullscreen(_ isFullscreen: Bool) {
|
func updateIsFullscreen(_ isFullscreen: Bool, force: Bool = false) {
|
||||||
guard self.isFullscreen != isFullscreen, let (layout, _) = self.validLayout else {
|
guard self.isFullscreen != isFullscreen || force, let (layout, _) = self.validLayout else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.isFullscreen = isFullscreen
|
self.isFullscreen = isFullscreen
|
||||||
@ -2770,16 +2894,20 @@ public final class VoiceChatController: ViewController {
|
|||||||
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
|
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isScheduled = false
|
||||||
|
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||||
|
isScheduled = true
|
||||||
|
}
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
|
||||||
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
|
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
|
||||||
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
|
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
|
||||||
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor)
|
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen || isScheduled ? panelBackgroundColor : secondaryPanelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||||
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
|
||||||
|
|
||||||
if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
|
if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
|
||||||
snapshotView.frame = self.topCornersNode.frame
|
snapshotView.frame = self.topCornersNode.frame
|
||||||
@ -2814,22 +2942,39 @@ public final class VoiceChatController: ViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var title = self.currentTitle
|
var title = self.currentTitle
|
||||||
if !self.isFullscreen && !self.currentTitleIsCustom {
|
if self.isScheduling {
|
||||||
|
title = self.presentationData.strings.ScheduleVoiceChat_Title
|
||||||
|
} else if !self.isFullscreen && !self.currentTitleIsCustom {
|
||||||
if let navigationController = self.controller?.navigationController as? NavigationController {
|
if let navigationController = self.controller?.navigationController as? NavigationController {
|
||||||
for controller in navigationController.viewControllers.reversed() {
|
for controller in navigationController.viewControllers.reversed() {
|
||||||
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId {
|
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId {
|
||||||
title = self.presentationData.strings.VoiceChat_Title
|
if self.callState?.scheduleTimestamp != nil {
|
||||||
|
title = self.presentationData.strings.VoiceChat_ScheduledTitle
|
||||||
|
} else {
|
||||||
|
title = self.presentationData.strings.VoiceChat_Title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subtitle = self.currentSubtitle
|
||||||
|
if self.isScheduling {
|
||||||
|
subtitle = ""
|
||||||
|
} else if self.callState?.scheduleTimestamp != nil {
|
||||||
|
if self.callState?.canManageCall ?? false {
|
||||||
|
subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle
|
||||||
|
} else {
|
||||||
|
subtitle = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var size = layout.size
|
var size = layout.size
|
||||||
if case .regular = layout.metrics.widthClass {
|
if case .regular = layout.metrics.widthClass {
|
||||||
size.width = floor(min(size.width, size.height) * 0.5)
|
size.width = floor(min(size.width, size.height) * 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: self.currentSubtitle, transition: transition)
|
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: subtitle, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateButtons(animated: Bool) {
|
private func updateButtons(animated: Bool) {
|
||||||
@ -2866,7 +3011,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
coloredButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
|
coloredButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
let soundImage: CallControllerButtonItemNode.Content.Image
|
var soundImage: CallControllerButtonItemNode.Content.Image
|
||||||
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = coloredButtonAppearance
|
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = coloredButtonAppearance
|
||||||
var soundTitle: String = self.presentationData.strings.Call_Speaker
|
var soundTitle: String = self.presentationData.strings.Call_Speaker
|
||||||
switch audioMode {
|
switch audioMode {
|
||||||
@ -2890,6 +3035,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
soundTitle = self.presentationData.strings.Call_Audio
|
soundTitle = self.presentationData.strings.Call_Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||||
|
soundImage = .share
|
||||||
|
soundTitle = self.presentationData.strings.VoiceChat_ShareShort
|
||||||
|
soundAppearance = coloredButtonAppearance
|
||||||
|
}
|
||||||
|
|
||||||
let videoButtonSize: CGSize
|
let videoButtonSize: CGSize
|
||||||
var buttonsTitleAlpha: CGFloat
|
var buttonsTitleAlpha: CGFloat
|
||||||
switch self.displayMode {
|
switch self.displayMode {
|
||||||
@ -2916,6 +3067,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
|
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var ignoreNextConnecting = false
|
||||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
let isFirstTime = self.validLayout == nil
|
let isFirstTime = self.validLayout == nil
|
||||||
self.validLayout = (layout, navigationHeight)
|
self.validLayout = (layout, navigationHeight)
|
||||||
@ -2993,7 +3145,16 @@ public final class VoiceChatController: ViewController {
|
|||||||
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
|
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
|
||||||
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
|
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
|
||||||
|
|
||||||
let centralButtonSize = CGSize(width: 300.0, height: 300.0)
|
if let pickerView = self.pickerView {
|
||||||
|
transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)
|
||||||
|
transition.updateFrame(node: self.timerNode, frame: timerFrame)
|
||||||
|
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
|
||||||
|
|
||||||
|
let centralButtonSide = min(size.width, size.height) - 32.0
|
||||||
|
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
|
||||||
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
|
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
|
||||||
let sideButtonMinimalInset: CGFloat = 16.0
|
let sideButtonMinimalInset: CGFloat = 16.0
|
||||||
let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0))
|
let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0))
|
||||||
@ -3037,48 +3198,76 @@ public final class VoiceChatController: ViewController {
|
|||||||
let actionButtonTitle: String
|
let actionButtonTitle: String
|
||||||
let actionButtonSubtitle: String
|
let actionButtonSubtitle: String
|
||||||
var actionButtonEnabled = true
|
var actionButtonEnabled = true
|
||||||
if let callState = self.callState {
|
if let callState = self.callState, !self.isScheduling {
|
||||||
switch callState.networkState {
|
var isScheduled = callState.scheduleTimestamp != nil
|
||||||
case .connecting:
|
if isScheduled {
|
||||||
|
self.ignoreNextConnecting = true
|
||||||
|
if callState.canManageCall {
|
||||||
|
actionButtonState = .scheduled(state: .start)
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
} else {
|
||||||
|
actionButtonState = .scheduled(state: .subscribe)
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let connected = self.ignoreNextConnecting || callState.networkState == .connected
|
||||||
|
if case .connected = callState.networkState {
|
||||||
|
self.ignoreNextConnecting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if connected {
|
||||||
|
if let muteState = callState.muteState, !self.pushingToTalk {
|
||||||
|
if muteState.canUnmute {
|
||||||
|
actionButtonState = .active(state: .muted)
|
||||||
|
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
} else {
|
||||||
|
actionButtonState = .active(state: .cantSpeak)
|
||||||
|
|
||||||
|
if callState.raisedHand {
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
|
||||||
|
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
|
||||||
|
} else {
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
|
||||||
|
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionButtonState = .active(state: .on)
|
||||||
|
|
||||||
|
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionButtonState = .connecting
|
||||||
|
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
actionButtonEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.isScheduling {
|
||||||
|
actionButtonState = .button(text: self.scheduleButtonTitle)
|
||||||
|
actionButtonTitle = ""
|
||||||
|
actionButtonSubtitle = ""
|
||||||
|
actionButtonEnabled = true
|
||||||
|
} else {
|
||||||
actionButtonState = .connecting
|
actionButtonState = .connecting
|
||||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||||
actionButtonSubtitle = ""
|
actionButtonSubtitle = ""
|
||||||
actionButtonEnabled = false
|
actionButtonEnabled = false
|
||||||
case .connected:
|
|
||||||
if let muteState = callState.muteState, !self.pushingToTalk {
|
|
||||||
if muteState.canUnmute {
|
|
||||||
actionButtonState = .active(state: .muted)
|
|
||||||
|
|
||||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
|
|
||||||
actionButtonSubtitle = ""
|
|
||||||
} else {
|
|
||||||
actionButtonState = .active(state: .cantSpeak)
|
|
||||||
|
|
||||||
if callState.raisedHand {
|
|
||||||
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
|
|
||||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
|
|
||||||
} else {
|
|
||||||
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
|
|
||||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actionButtonState = .active(state: .on)
|
|
||||||
|
|
||||||
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
|
|
||||||
actionButtonSubtitle = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
actionButtonState = .connecting
|
|
||||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
|
||||||
actionButtonSubtitle = ""
|
|
||||||
actionButtonEnabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.actionButton.isDisabled = !actionButtonEnabled
|
self.actionButton.isDisabled = !actionButtonEnabled
|
||||||
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
|
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
|
||||||
|
|
||||||
|
let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate)
|
||||||
|
self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight)
|
||||||
|
|
||||||
if self.actionButton.supernode === self.bottomPanelNode {
|
if self.actionButton.supernode === self.bottomPanelNode {
|
||||||
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame)
|
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame)
|
||||||
}
|
}
|
||||||
@ -3196,6 +3385,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
self.enqueuedTransitions.remove(at: 0)
|
self.enqueuedTransitions.remove(at: 0)
|
||||||
|
|
||||||
|
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
|
||||||
|
self.listNode.alpha = 0.0
|
||||||
|
self.backgroundNode.backgroundColor = panelBackgroundColor
|
||||||
|
self.updateIsFullscreen(false)
|
||||||
|
}
|
||||||
|
|
||||||
var options = ListViewDeleteAndInsertOptions()
|
var options = ListViewDeleteAndInsertOptions()
|
||||||
let isFirstTime = self.isFirstTime
|
let isFirstTime = self.isFirstTime
|
||||||
if isFirstTime {
|
if isFirstTime {
|
||||||
@ -3235,7 +3430,11 @@ public final class VoiceChatController: ViewController {
|
|||||||
let listTopInset = layoutTopInset + 63.0
|
let listTopInset = layoutTopInset + 63.0
|
||||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||||
|
|
||||||
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
|
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||||
|
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
|
||||||
|
} else {
|
||||||
|
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
|
||||||
|
}
|
||||||
|
|
||||||
let targetY = listTopInset + (self.topInset ?? listSize.height)
|
let targetY = listTopInset + (self.topInset ?? listSize.height)
|
||||||
|
|
||||||
@ -3453,9 +3652,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
if gestureRecognizer is DirectionalPanGestureRecognizer {
|
if gestureRecognizer is UILongPressGestureRecognizer {
|
||||||
|
return !self.isScheduling
|
||||||
|
} else if gestureRecognizer is DirectionalPanGestureRecognizer {
|
||||||
let location = gestureRecognizer.location(in: self.bottomPanelNode.view)
|
let location = gestureRecognizer.location(in: self.bottomPanelNode.view)
|
||||||
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) {
|
let containerLocation = gestureRecognizer.location(in: self.contentContainer.view)
|
||||||
|
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) || self.pickerView?.frame.contains(containerLocation) == true {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3494,6 +3696,9 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.controller?.dismissAllTooltips()
|
self.controller?.dismissAllTooltips()
|
||||||
case .changed:
|
case .changed:
|
||||||
var translation = recognizer.translation(in: self.contentContainer.view).y
|
var translation = recognizer.translation(in: self.contentContainer.view).y
|
||||||
|
if (self.isScheduling || self.callState?.scheduleTimestamp != nil) && translation < 0.0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
var topInset: CGFloat = 0.0
|
var topInset: CGFloat = 0.0
|
||||||
if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
|
if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
|
||||||
topInset = currentTopInset
|
topInset = currentTopInset
|
||||||
@ -3591,9 +3796,13 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.panGestureArguments = nil
|
self.panGestureArguments = nil
|
||||||
var dismissing = false
|
var dismissing = false
|
||||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
|
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
|
||||||
self.controller?.dismiss(closing: false, manual: true)
|
if self.isScheduling {
|
||||||
|
self.dismissScheduled()
|
||||||
|
} else {
|
||||||
|
self.controller?.dismiss(closing: false, manual: true)
|
||||||
|
}
|
||||||
dismissing = true
|
dismissing = true
|
||||||
} else if velocity.y < -300.0 || offset < topInset / 2.0 {
|
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
|
||||||
if velocity.y > -1500.0 && !self.isFullscreen {
|
if velocity.y > -1500.0 && !self.isFullscreen {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||||
@ -3610,7 +3819,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||||
self.animatingExpansion = false
|
self.animatingExpansion = false
|
||||||
})
|
})
|
||||||
} else {
|
} else if !self.isScheduling {
|
||||||
self.updateIsFullscreen(false)
|
self.updateIsFullscreen(false)
|
||||||
self.animatingExpansion = true
|
self.animatingExpansion = true
|
||||||
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||||
@ -3684,6 +3893,24 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openTitleEditing() {
|
||||||
|
let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
|
||||||
|
if let strongSelf = self, let title = title {
|
||||||
|
strongSelf.call.updateTitle(title)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
strongSelf.controller?.present(controller, in: .window(.root))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
|
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
guard let peerId = self.callState?.myPeerId else {
|
guard let peerId = self.callState?.myPeerId else {
|
||||||
return
|
return
|
||||||
@ -4096,6 +4323,8 @@ public final class VoiceChatController: ViewController {
|
|||||||
let count = navigationController.viewControllers.count
|
let count = navigationController.viewControllers.count
|
||||||
if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
|
if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
|
||||||
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue {
|
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue {
|
||||||
|
} else if case .button = self.controllerNode.actionButton.stateValue {
|
||||||
|
} else if case .scheduled = self.controllerNode.actionButton.stateValue {
|
||||||
} else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible {
|
} else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible {
|
||||||
} else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
|
} else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
|
||||||
} else {
|
} else {
|
||||||
|
@ -145,7 +145,7 @@ public final class VoiceChatJoinScreen: ViewController {
|
|||||||
defaultJoinAsPeerId = cachedData.callJoinPeerId
|
defaultJoinAsPeerId = cachedData.callJoinPeerId
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title)
|
let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title, scheduleTimestamp: call.info.scheduleTimestamp, subscribed: false)
|
||||||
if availablePeers.count > 0 && defaultJoinAsPeerId == nil {
|
if availablePeers.count > 0 && defaultJoinAsPeerId == nil {
|
||||||
strongSelf.dismiss()
|
strongSelf.dismiss()
|
||||||
strongSelf.join(activeCall)
|
strongSelf.join(activeCall)
|
||||||
|
@ -396,7 +396,7 @@ public final class VoiceChatOverlayController: ViewController {
|
|||||||
var slide = true
|
var slide = true
|
||||||
var hidden = true
|
var hidden = true
|
||||||
var animated = true
|
var animated = true
|
||||||
var animateInsets = true
|
|
||||||
if controllers.count == 1 || controllers.last is ChatController {
|
if controllers.count == 1 || controllers.last is ChatController {
|
||||||
if let chatController = controllers.last as? ChatController {
|
if let chatController = controllers.last as? ChatController {
|
||||||
slide = false
|
slide = false
|
||||||
@ -416,9 +416,13 @@ public final class VoiceChatOverlayController: ViewController {
|
|||||||
hidden = true
|
hidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .active(.cantSpeak) = state {
|
switch state {
|
||||||
hidden = true
|
case .active(.cantSpeak), .button, .scheduled:
|
||||||
|
hidden = true
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasVoiceChatController {
|
if hasVoiceChatController {
|
||||||
hidden = false
|
hidden = false
|
||||||
animated = self.initiallyHidden
|
animated = self.initiallyHidden
|
||||||
@ -429,7 +433,6 @@ public final class VoiceChatOverlayController: ViewController {
|
|||||||
|
|
||||||
let previousInsets = self.additionalSideInsets
|
let previousInsets = self.additionalSideInsets
|
||||||
self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
|
self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
|
||||||
|
|
||||||
if previousInsets != self.additionalSideInsets {
|
if previousInsets != self.additionalSideInsets {
|
||||||
self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||||
}
|
}
|
||||||
|
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
|
private let maxLength: Int
|
||||||
|
|
||||||
init(theme: PresentationTheme, placeholder: String, maxLength: Int) {
|
init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.maxLength = maxLength
|
self.maxLength = maxLength
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
|||||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||||
self.textInputNode.keyboardType = .default
|
self.textInputNode.keyboardType = .default
|
||||||
self.textInputNode.autocapitalizationType = .sentences
|
self.textInputNode.autocapitalizationType = .sentences
|
||||||
self.textInputNode.returnKeyType = .done
|
self.textInputNode.returnKeyType = returnKeyType
|
||||||
self.textInputNode.autocorrectionType = .default
|
self.textInputNode.autocorrectionType = .default
|
||||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||||
|
|
||||||
@ -510,7 +510,7 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
|||||||
self.titleNode = ASTextNode()
|
self.titleNode = ASTextNode()
|
||||||
self.titleNode.maximumNumberOfLines = 2
|
self.titleNode.maximumNumberOfLines = 2
|
||||||
|
|
||||||
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength)
|
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next)
|
||||||
self.firstNameInputFieldNode.text = firstNameValue ?? ""
|
self.firstNameInputFieldNode.text = firstNameValue ?? ""
|
||||||
|
|
||||||
self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
|
self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
|
||||||
@ -550,14 +550,6 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
|||||||
self.addSubnode(separatorNode)
|
self.addSubnode(separatorNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.firstNameInputFieldNode.updateHeight = { [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
if let _ = strongSelf.validLayout {
|
|
||||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateTheme(theme)
|
self.updateTheme(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return account.postbox.transaction { transaction -> GroupCallInfo in
|
||||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||||
if let cachedData = cachedData as? CachedChannelData {
|
if let cachedData = cachedData as? CachedChannelData {
|
||||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false))
|
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
|
||||||
} else if let cachedData = cachedData as? CachedGroupData {
|
} else if let cachedData = cachedData as? CachedGroupData {
|
||||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false))
|
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
|
||||||
} else {
|
} else {
|
||||||
return cachedData
|
return cachedData
|
||||||
}
|
}
|
||||||
@ -331,15 +331,27 @@ public func updateGroupCallJoinAsPeer(account: Account, peerId: PeerId, joinAs:
|
|||||||
}
|
}
|
||||||
|> castError(UpdateGroupCallJoinAsPeerError.self)
|
|> castError(UpdateGroupCallJoinAsPeerError.self)
|
||||||
|> mapToSignal { result in
|
|> mapToSignal { result in
|
||||||
guard let (peer, joinAs) = result else {
|
guard let (inputPeer, joinInputPeer) = result else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: peer, joinAs: joinAs))
|
return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: inputPeer, joinAs: joinInputPeer))
|
||||||
|> mapError { _ -> UpdateGroupCallJoinAsPeerError in
|
|> mapError { _ -> UpdateGroupCallJoinAsPeerError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|> mapToSignal { result -> Signal<Never, UpdateGroupCallJoinAsPeerError> in
|
|> mapToSignal { result -> Signal<Never, UpdateGroupCallJoinAsPeerError> in
|
||||||
return .complete()
|
return account.postbox.transaction { transaction in
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||||
|
if let cachedData = cachedData as? CachedChannelData {
|
||||||
|
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||||
|
} else if let cachedData = cachedData as? CachedGroupData {
|
||||||
|
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||||
|
} else {
|
||||||
|
return cachedData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|> castError(UpdateGroupCallJoinAsPeerError.self)
|
||||||
|
|> ignoreValues
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -644,9 +656,9 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
|||||||
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
||||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||||
if let cachedData = cachedData as? CachedChannelData {
|
if let cachedData = cachedData as? CachedChannelData {
|
||||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
|
||||||
} else if let cachedData = cachedData as? CachedGroupData {
|
} else if let cachedData = cachedData as? CachedGroupData {
|
||||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
|
||||||
} else {
|
} else {
|
||||||
return cachedData
|
return cachedData
|
||||||
}
|
}
|
||||||
|
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 {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||||
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) {
|
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) {
|
||||||
@ -95,14 +95,14 @@ public func messageContentKind(contentSettings: ContentSettings, message: Messag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) {
|
if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) {
|
||||||
return kind
|
return kind
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .text(message.text)
|
return .text(message.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? {
|
public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? {
|
||||||
switch media {
|
switch media {
|
||||||
case let expiredMedia as TelegramMediaExpiredContent:
|
case let expiredMedia as TelegramMediaExpiredContent:
|
||||||
switch expiredMedia.data {
|
switch expiredMedia.data {
|
||||||
@ -163,7 +163,7 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P
|
|||||||
}
|
}
|
||||||
case _ as TelegramMediaAction:
|
case _ as TelegramMediaAction:
|
||||||
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
||||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
|
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -223,8 +223,8 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) {
|
public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> (String, Bool) {
|
||||||
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId)
|
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
||||||
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
|
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
|
||||||
return (foldLineBreaks(message.text), false)
|
return (foldLineBreaks(message.text), false)
|
||||||
}
|
}
|
||||||
|
@ -27,11 +27,11 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
|
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
|
||||||
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
|
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
|
||||||
}
|
}
|
||||||
|
|
||||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? {
|
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? {
|
||||||
var attributedString: NSAttributedString?
|
var attributedString: NSAttributedString?
|
||||||
|
|
||||||
let primaryTextColor: UIColor
|
let primaryTextColor: UIColor
|
||||||
@ -448,7 +448,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||||
case let .groupPhoneCall(_, _, scheduleDate, duration):
|
case let .groupPhoneCall(_, _, scheduleDate, duration):
|
||||||
if let scheduleDate = scheduleDate {
|
if let scheduleDate = scheduleDate {
|
||||||
let titleString = strings.Notification_VoiceChatScheduled
|
let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
|
||||||
|
let titleString = strings.Notification_VoiceChatScheduled(timeString).0
|
||||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||||
} else if let duration = duration {
|
} else if let duration = duration {
|
||||||
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
|
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
|
||||||
|
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) {
|
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) {
|
||||||
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
||||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||||
|
@ -356,7 +356,7 @@ final class AuthorizedApplicationContext {
|
|||||||
|
|
||||||
if inAppNotificationSettings.displayPreviews {
|
if inAppNotificationSettings.displayPreviews {
|
||||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: {
|
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
var foundOverlay = false
|
var foundOverlay = false
|
||||||
strongSelf.mainWindow.forEachViewController({ controller in
|
strongSelf.mainWindow.forEachViewController({ controller in
|
||||||
|
@ -177,14 +177,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
controller.inProgress = false
|
controller.inProgress = false
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
var actions: [TextAlertAction] = [
|
var actions: [TextAlertAction] = []
|
||||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
|
||||||
]
|
|
||||||
switch error {
|
switch error {
|
||||||
case .limitExceeded:
|
case .limitExceeded:
|
||||||
text = strongSelf.presentationData.strings.Login_CodeFloodError
|
text = strongSelf.presentationData.strings.Login_CodeFloodError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
case .invalidPhoneNumber:
|
case .invalidPhoneNumber:
|
||||||
text = strongSelf.presentationData.strings.Login_InvalidPhoneError
|
text = strongSelf.presentationData.strings.Login_InvalidPhoneError
|
||||||
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||||
guard let strongSelf = self, let controller = controller else {
|
guard let strongSelf = self, let controller = controller else {
|
||||||
return
|
return
|
||||||
@ -200,8 +200,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
}))
|
}))
|
||||||
case .phoneLimitExceeded:
|
case .phoneLimitExceeded:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
case .phoneBanned:
|
case .phoneBanned:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
||||||
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||||
guard let strongSelf = self, let controller = controller else {
|
guard let strongSelf = self, let controller = controller else {
|
||||||
return
|
return
|
||||||
@ -217,6 +219,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
}))
|
}))
|
||||||
case let .generic(info):
|
case let .generic(info):
|
||||||
text = strongSelf.presentationData.strings.Login_UnknownError
|
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||||
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||||
guard let strongSelf = self, let controller = controller else {
|
guard let strongSelf = self, let controller = controller else {
|
||||||
return
|
return
|
||||||
@ -238,6 +241,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
}))
|
}))
|
||||||
case .timeout:
|
case .timeout:
|
||||||
text = strongSelf.presentationData.strings.Login_NetworkError
|
text = strongSelf.presentationData.strings.Login_NetworkError
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in
|
||||||
guard let strongSelf = self, let controller = controller else {
|
guard let strongSelf = self, let controller = controller else {
|
||||||
return
|
return
|
||||||
|
@ -535,7 +535,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
||||||
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
||||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title))
|
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribed: activeCall.subscribed))
|
||||||
} else {
|
} else {
|
||||||
var canManageGroupCalls = false
|
var canManageGroupCalls = false
|
||||||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
|
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
|
||||||
@ -564,12 +564,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
statusController?.dismiss()
|
statusController?.dismiss()
|
||||||
}
|
}
|
||||||
strongSelf.present(statusController, in: .window(.root))
|
strongSelf.present(statusController, in: .window(.root))
|
||||||
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId)
|
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId, title: nil, scheduleDate: nil)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribed: false))
|
||||||
}, error: { [weak self] error in
|
}, error: { [weak self] error in
|
||||||
dismissStatus?()
|
dismissStatus?()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
|||||||
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||||
return editPanelNode
|
return editPanelNode
|
||||||
} else {
|
} else {
|
||||||
let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder)
|
let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
|
||||||
panelNode.interfaceInteraction = interfaceInteraction
|
panelNode.interfaceInteraction = interfaceInteraction
|
||||||
return panelNode
|
return panelNode
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
|||||||
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||||
return replyPanelNode
|
return replyPanelNode
|
||||||
} else {
|
} else {
|
||||||
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder)
|
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
|
||||||
panelNode.interfaceInteraction = interfaceInteraction
|
panelNode.interfaceInteraction = interfaceInteraction
|
||||||
return panelNode
|
return panelNode
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@ import UniversalMediaPlayer
|
|||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import GalleryUI
|
import GalleryUI
|
||||||
|
|
||||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false)
|
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
@ -132,7 +132,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||||
|
|
||||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, accountPeerId: item.context.account.peerId)
|
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId)
|
||||||
|
|
||||||
var image: TelegramMediaImage?
|
var image: TelegramMediaImage?
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
|
@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData {
|
|||||||
if let chatPeer = message.peers[item.message.id.peerId] {
|
if let chatPeer = message.peers[item.message.id.peerId] {
|
||||||
let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||||
|
|
||||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
||||||
|
|
||||||
var text = messageText
|
var text = messageText
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import TelegramStringFormatting
|
|||||||
public final class ChatMessageNotificationItem: NotificationItem {
|
public final class ChatMessageNotificationItem: NotificationItem {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
|
let dateTimeFormat: PresentationDateTimeFormat
|
||||||
let nameDisplayOrder: PresentationPersonNameOrder
|
let nameDisplayOrder: PresentationPersonNameOrder
|
||||||
let messages: [Message]
|
let messages: [Message]
|
||||||
let tapAction: () -> Bool
|
let tapAction: () -> Bool
|
||||||
@ -27,9 +28,10 @@ public final class ChatMessageNotificationItem: NotificationItem {
|
|||||||
return messages.first?.id.peerId
|
return messages.first?.id.peerId
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) {
|
public init(context: AccountContext, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.dateTimeFormat = dateTimeFormat
|
||||||
self.nameDisplayOrder = nameDisplayOrder
|
self.nameDisplayOrder = nameDisplayOrder
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.tapAction = tapAction
|
self.tapAction = tapAction
|
||||||
@ -181,7 +183,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||||||
if message.containsSecretMedia {
|
if message.containsSecretMedia {
|
||||||
imageDimensions = nil
|
imageDimensions = nil
|
||||||
}
|
}
|
||||||
messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, accountPeerId: item.context.account.peerId).0
|
messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId).0
|
||||||
} else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] {
|
} else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] {
|
||||||
var displayAuthor = true
|
var displayAuthor = true
|
||||||
if let channel = peer as? TelegramChannel {
|
if let channel = peer as? TelegramChannel {
|
||||||
@ -218,9 +220,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if item.messages[0].groupingKey != nil {
|
} else if item.messages[0].groupingKey != nil {
|
||||||
var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId).key
|
var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId).key
|
||||||
for i in 1 ..< item.messages.count {
|
for i in 1 ..< item.messages.count {
|
||||||
let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
|
||||||
if kind != nextKind.key {
|
if kind != nextKind.key {
|
||||||
kind = .text
|
kind = .text
|
||||||
break
|
break
|
||||||
|
@ -65,7 +65,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: context.account.peerId)
|
let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId)
|
||||||
|
|
||||||
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||||
let titleColor: UIColor
|
let titleColor: UIColor
|
||||||
|
@ -269,7 +269,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
self.currentMessage = interfaceState.pinnedMessage
|
self.currentMessage = interfaceState.pinnedMessage
|
||||||
|
|
||||||
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
|
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
|
||||||
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
|
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,14 +314,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
self.currentLayout = (width, leftInset, rightInset)
|
self.currentLayout = (width, leftInset, rightInset)
|
||||||
|
|
||||||
if let currentMessage = self.currentMessage {
|
if let currentMessage = self.currentMessage {
|
||||||
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
|
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return panelHeight
|
return panelHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
|
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
|
||||||
let message = pinnedMessage.message
|
let message = pinnedMessage.message
|
||||||
|
|
||||||
var animationTransition: ContainedViewLayoutTransition = .immediate
|
var animationTransition: ContainedViewLayoutTransition = .immediate
|
||||||
@ -470,7 +470,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
}
|
}
|
||||||
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
||||||
|
|
||||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
||||||
|
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
@ -262,12 +262,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let calendar = Calendar(identifier: .gregorian)
|
||||||
private func updateButtonTitle() {
|
private func updateButtonTitle() {
|
||||||
guard let date = self.pickerView?.date else {
|
guard let date = self.pickerView?.date else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = Calendar(identifier: .gregorian)
|
|
||||||
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||||
switch mode {
|
switch mode {
|
||||||
case .scheduledMessages:
|
case .scheduledMessages:
|
||||||
|
@ -15,6 +15,7 @@ import PhotoResources
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
|
||||||
final class EditAccessoryPanelNode: AccessoryPanelNode {
|
final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||||
|
let dateTimeFormat: PresentationDateTimeFormat
|
||||||
let messageId: MessageId
|
let messageId: MessageId
|
||||||
|
|
||||||
let closeButton: ASButtonNode
|
let closeButton: ASButtonNode
|
||||||
@ -67,12 +68,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
var strings: PresentationStrings
|
var strings: PresentationStrings
|
||||||
var nameDisplayOrder: PresentationPersonNameOrder
|
var nameDisplayOrder: PresentationPersonNameOrder
|
||||||
|
|
||||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
|
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.nameDisplayOrder = nameDisplayOrder
|
self.nameDisplayOrder = nameDisplayOrder
|
||||||
|
self.dateTimeFormat = dateTimeFormat
|
||||||
|
|
||||||
self.closeButton = ASButtonNode()
|
self.closeButton = ASButtonNode()
|
||||||
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
|
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
|
||||||
@ -159,7 +161,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
if let currentEditMediaReference = self.currentEditMediaReference {
|
if let currentEditMediaReference = self.currentEditMediaReference {
|
||||||
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
||||||
}
|
}
|
||||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, accountPeerId: self.context.account.peerId)
|
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedMediaReference: AnyMediaReference?
|
var updatedMediaReference: AnyMediaReference?
|
||||||
@ -231,7 +233,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
if let currentEditMediaReference = self.currentEditMediaReference {
|
if let currentEditMediaReference = self.currentEditMediaReference {
|
||||||
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
||||||
}
|
}
|
||||||
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) {
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) {
|
||||||
case .text:
|
case .text:
|
||||||
isMedia = false
|
isMedia = false
|
||||||
default:
|
default:
|
||||||
|
@ -1019,7 +1019,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
displayLeave = false
|
displayLeave = false
|
||||||
}
|
}
|
||||||
result.append(.mute)
|
result.append(.mute)
|
||||||
if hasVoiceChat {
|
if hasVoiceChat || canStartVoiceChat {
|
||||||
result.append(.voiceChat)
|
result.append(.voiceChat)
|
||||||
}
|
}
|
||||||
if hasDiscussion {
|
if hasDiscussion {
|
||||||
@ -1038,7 +1038,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
|
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
|
||||||
canReport = false
|
canReport = false
|
||||||
}
|
}
|
||||||
if !canReport && !canViewStats && !canStartVoiceChat {
|
if !canReport && !canViewStats {
|
||||||
displayMore = false
|
displayMore = false
|
||||||
}
|
}
|
||||||
if displayMore {
|
if displayMore {
|
||||||
@ -1051,10 +1051,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
var isPublic = false
|
var isPublic = false
|
||||||
var isCreator = false
|
var isCreator = false
|
||||||
var hasVoiceChat = false
|
var hasVoiceChat = false
|
||||||
|
var canStartVoiceChat = false
|
||||||
|
|
||||||
if group.flags.contains(.hasVoiceChat) {
|
if group.flags.contains(.hasVoiceChat) {
|
||||||
hasVoiceChat = true
|
hasVoiceChat = true
|
||||||
}
|
}
|
||||||
|
if !hasVoiceChat {
|
||||||
|
if case .creator = group.role {
|
||||||
|
canStartVoiceChat = true
|
||||||
|
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) {
|
||||||
|
canStartVoiceChat = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if case .creator = group.role {
|
if case .creator = group.role {
|
||||||
isCreator = true
|
isCreator = true
|
||||||
@ -1073,13 +1081,11 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
if !group.hasBannedPermission(.banAddMembers) {
|
if !group.hasBannedPermission(.banAddMembers) {
|
||||||
canAddMembers = true
|
canAddMembers = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if canAddMembers {
|
if canAddMembers {
|
||||||
result.append(.addMember)
|
result.append(.addMember)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.append(.mute)
|
result.append(.mute)
|
||||||
if hasVoiceChat {
|
if hasVoiceChat || canStartVoiceChat {
|
||||||
result.append(.voiceChat)
|
result.append(.voiceChat)
|
||||||
}
|
}
|
||||||
result.append(.search)
|
result.append(.search)
|
||||||
|
@ -153,6 +153,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||||||
colors = ["Middle.Group 1.Fill 1": iconColor,
|
colors = ["Middle.Group 1.Fill 1": iconColor,
|
||||||
"Top.Group 1.Fill 1": iconColor,
|
"Top.Group 1.Fill 1": iconColor,
|
||||||
"Bottom.Group 1.Fill 1": iconColor,
|
"Bottom.Group 1.Fill 1": iconColor,
|
||||||
|
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||||
"Line.Group 1.Stroke 1": iconColor]
|
"Line.Group 1.Stroke 1": iconColor]
|
||||||
if previousIcon == .unmute {
|
if previousIcon == .unmute {
|
||||||
playOnce = true
|
playOnce = true
|
||||||
@ -164,6 +165,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||||||
colors = ["Middle.Group 1.Fill 1": iconColor,
|
colors = ["Middle.Group 1.Fill 1": iconColor,
|
||||||
"Top.Group 1.Fill 1": iconColor,
|
"Top.Group 1.Fill 1": iconColor,
|
||||||
"Bottom.Group 1.Fill 1": iconColor,
|
"Bottom.Group 1.Fill 1": iconColor,
|
||||||
|
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||||
"Line.Group 1.Stroke 1": iconColor]
|
"Line.Group 1.Stroke 1": iconColor]
|
||||||
if previousIcon == .mute {
|
if previousIcon == .mute {
|
||||||
playOnce = true
|
playOnce = true
|
||||||
@ -248,7 +250,9 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||||||
if isActiveUpdated, !self.containerNode.alpha.isZero {
|
if isActiveUpdated, !self.containerNode.alpha.isZero {
|
||||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3)
|
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3)
|
||||||
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
|
if !isExpanded {
|
||||||
|
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)
|
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||||
|
@ -3371,7 +3371,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
case .videoCall:
|
case .videoCall:
|
||||||
self.requestCall(isVideo: true)
|
self.requestCall(isVideo: true)
|
||||||
case .voiceChat:
|
case .voiceChat:
|
||||||
self.requestCall(isVideo: false)
|
self.requestCall(isVideo: false, gesture: gesture)
|
||||||
case .mute:
|
case .mute:
|
||||||
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
||||||
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
||||||
@ -3627,20 +3627,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let channel = peer as? TelegramChannel {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
if !channel.flags.contains(.hasVoiceChat) {
|
|
||||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
|
||||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
|
||||||
}, action: { [weak self] c, f in
|
|
||||||
self?.requestCall(isVideo: false, contextController: c, result: f, backAction: { c in
|
|
||||||
if let mainItemsImpl = mainItemsImpl {
|
|
||||||
c.setItems(mainItemsImpl())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
|
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
|
||||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
|
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
|
||||||
@ -3730,22 +3716,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let group = peer as? TelegramGroup {
|
} else if let group = peer as? TelegramGroup {
|
||||||
var canManageGroupCalls = false
|
|
||||||
if case .creator = group.role {
|
|
||||||
canManageGroupCalls = true
|
|
||||||
} else if case let .admin(rights, _) = group.role {
|
|
||||||
if rights.rights.contains(.canManageCalls) {
|
|
||||||
canManageGroupCalls = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
|
||||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
|
||||||
}, action: { [weak self] c, f in
|
|
||||||
self?.requestCall(isVideo: false, contextController: c, result: f)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
if case .Member = group.membership {
|
if case .Member = group.membership {
|
||||||
if !items.isEmpty {
|
if !items.isEmpty {
|
||||||
items.append(.separator)
|
items.append(.separator)
|
||||||
@ -3976,14 +3946,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
}, activeCall: activeCall)
|
}, activeCall: activeCall)
|
||||||
} else {
|
} else {
|
||||||
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
|
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: gesture, contextController: contextController)
|
||||||
result?(.dismissWithoutContent)
|
|
||||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
|
|
||||||
} else {
|
|
||||||
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
|
||||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId)
|
|
||||||
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4006,6 +3969,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
|
self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scheduleGroupCall() {
|
||||||
|
self.context.scheduleGroupCall(peerId: self.peerId)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// let time = Int32(Date().timeIntervalSince1970 + 86400)
|
||||||
|
// self.activeActionDisposable.set((createGroupCall(account: self.context.account, peerId: self.peerId, title: nil, scheduleDate: time)
|
||||||
|
// |> deliverOnMainQueue).start(next: { [weak self] info in
|
||||||
|
//
|
||||||
|
// }))
|
||||||
|
}
|
||||||
|
|
||||||
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
|
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
|
||||||
if let _ = self.context.sharedContext.callManager {
|
if let _ = self.context.sharedContext.callManager {
|
||||||
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
|
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
|
||||||
@ -4013,26 +3987,41 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var dismissStatus: (() -> Void)?
|
var cancelImpl: (() -> Void)?
|
||||||
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
|
let presentationData = strongSelf.presentationData
|
||||||
dismissStatus?()
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||||
}))
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
dismissStatus = { [weak self, weak statusController] in
|
cancelImpl?()
|
||||||
self?.activeActionDisposable.set(nil)
|
}))
|
||||||
statusController?.dismiss()
|
self?.controller?.present(controller, in: .window(.root))
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
strongSelf.controller?.present(statusController, in: .window(.root))
|
|> runOn(Queue.mainQueue())
|
||||||
strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId)
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
let createSignal = createGroupCall(account: strongSelf.context.account, peerId: peerId, title: nil, scheduleDate: nil)
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelImpl = { [weak self] in
|
||||||
|
self?.activeActionDisposable.set(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.activeActionDisposable.set((createSignal
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
|
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
|
||||||
result(joinAsPeerId)
|
result(joinAsPeerId)
|
||||||
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribed: false))
|
||||||
}, error: { [weak self] error in
|
}, error: { [weak self] error in
|
||||||
dismissStatus?()
|
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -4046,8 +4035,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||||||
}
|
}
|
||||||
strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||||
}, completed: { [weak self] in
|
|
||||||
dismissStatus?()
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4348,7 +4335,90 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
controller.push(statsController)
|
controller.push(statsController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextController? = nil) {
|
||||||
|
let context = self.context
|
||||||
|
let peerId = self.peerId
|
||||||
|
let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId
|
||||||
|
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||||
|
|> map { peer in
|
||||||
|
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||||
|
}
|
||||||
|
let _ = (combineLatest(queue: Queue.mainQueue(), currentAccountPeer, self.displayAsPeersPromise.get() |> take(1))
|
||||||
|
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
|
||||||
|
var result = currentAccountPeer
|
||||||
|
result.append(contentsOf: availablePeers)
|
||||||
|
return result
|
||||||
|
}).start(next: { [weak self] peers in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
if peers.count > 1 {
|
||||||
|
var selectedPeer: FoundPeer?
|
||||||
|
for peer in peers {
|
||||||
|
if peer.peer.id == defaultJoinAsPeerId {
|
||||||
|
selectedPeer = peer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let peer = selectedPeer {
|
||||||
|
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
||||||
|
let _ = updateGroupCallJoinAsPeer(account: context.account, peerId: peerId, joinAs: joinAsPeerId).start()
|
||||||
|
self?.openVoiceChatOptions(defaultJoinAsPeerId: joinAsPeerId, gesture: nil, contextController: c)
|
||||||
|
}, gesture: gesture, contextController: c, result: f, backAction: { [weak self] c in
|
||||||
|
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: nil, contextController: c)
|
||||||
|
})
|
||||||
|
|
||||||
|
})))
|
||||||
|
items.append(.separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
|
||||||
|
})))
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_ScheduleVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
self?.scheduleGroupCall()
|
||||||
|
})))
|
||||||
|
|
||||||
|
if let contextController = contextController {
|
||||||
|
contextController.setItems(.single(items))
|
||||||
|
} else {
|
||||||
|
strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat)
|
||||||
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller {
|
||||||
|
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||||
|
contextController.dismissed = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.state = strongSelf.state.withHighlightedButton(nil)
|
||||||
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.presentInGlobalOverlay(contextController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
|
private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
|
||||||
|
let dismissOnSelection = contextController == nil
|
||||||
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||||
|> map { peer in
|
|> map { peer in
|
||||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||||
@ -4398,8 +4468,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||||
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|
||||||
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
|
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
|
||||||
f(.dismissWithoutContent)
|
if dismissOnSelection {
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
}
|
||||||
completion(peer.peer.id)
|
completion(peer.peer.id)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
@ -7168,7 +7239,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
contactsController?.dismiss()
|
contactsController?.dismiss()
|
||||||
},completed: {
|
}, completed: {
|
||||||
contactsController?.dismiss()
|
contactsController?.dismiss()
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
@ -29,7 +29,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
|
|
||||||
var theme: PresentationTheme
|
var theme: PresentationTheme
|
||||||
|
|
||||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
|
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
|
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -86,7 +86,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||||
}
|
}
|
||||||
if let message = message {
|
if let message = message {
|
||||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId)
|
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedMediaReference: AnyMediaReference?
|
var updatedMediaReference: AnyMediaReference?
|
||||||
@ -152,7 +152,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
|
|
||||||
let isMedia: Bool
|
let isMedia: Bool
|
||||||
if let message = message {
|
if let message = message {
|
||||||
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) {
|
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) {
|
||||||
case .text:
|
case .text:
|
||||||
isMedia = false
|
isMedia = false
|
||||||
default:
|
default:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user