mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6338,26 +6338,31 @@ Sorry for the inconvenience.";
|
||||
"VoiceChat.PinVideo" = "Pin Video";
|
||||
"VoiceChat.UnpinVideo" = "Unpin Video";
|
||||
|
||||
"Notification.VoiceChatScheduled" = "Voice chat scheduled";
|
||||
"Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
|
||||
|
||||
"VoiceChat.EditStartTime" = "Edit Start Time";
|
||||
"VoiceChat.StartsIn" = "Starts in";
|
||||
"VoiceChat.LateBy" = "Late by";
|
||||
|
||||
"VoiceChat.StartNow" = "Start Now";
|
||||
"VoiceChat.SetReminder" = "Set Reminder";
|
||||
"VoiceChat.CancelReminder" = "Cancel Reminder";
|
||||
|
||||
"VoiceChat.ShareShort" = "share";
|
||||
|
||||
"VoiceChat.TapToEditTitle" = "Tap to edit title";
|
||||
|
||||
"ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat";
|
||||
|
||||
"ScheduleVoiceChat.Title" = "Schedule Voice Chat";
|
||||
"ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@.";
|
||||
"ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@.";
|
||||
|
||||
"ScheduleVoiceChat.ScheduleToday" = "Remind today at %@";
|
||||
"ScheduleVoiceChat.ScheduleTomorrow" = "Remind tomorrow at %@";
|
||||
"ScheduleVoiceChat.ScheduleOn" = "Remind on %@ at %@";
|
||||
"ScheduleVoiceChat.ScheduleToday" = "Start today at %@";
|
||||
"ScheduleVoiceChat.ScheduleTomorrow" = "Start tomorrow at %@";
|
||||
"ScheduleVoiceChat.ScheduleOn" = "Start on %@ at %@";
|
||||
|
||||
"VoiceChat.ScheduledTitle" = "Scheduled Voice Chat";
|
||||
|
||||
"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat";
|
||||
"Conversation.ScheduledVoiceChatStartsInShort" = "Voice chat starts %@";
|
||||
"Conversation.ScheduledVoiceChatStartsInShort" = "Starts %@";
|
||||
"Conversation.ScheduledVoiceChatStartsOn" = "Voice chat starts %@";
|
||||
"Conversation.ScheduledVoiceChatStartsOnShort" = "Starts %@";
|
||||
|
||||
@@ -736,6 +736,7 @@ public protocol AccountContext: class {
|
||||
func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError>
|
||||
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
||||
|
||||
func scheduleGroupCall(peerId: PeerId)
|
||||
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall)
|
||||
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ public enum JoinGroupCallManagerResult {
|
||||
case alreadyInProgress(PeerId?)
|
||||
}
|
||||
|
||||
public enum RequestScheduleGroupCallResult {
|
||||
case success
|
||||
case alreadyInProgress(PeerId?)
|
||||
}
|
||||
|
||||
public struct CallAuxiliaryServer {
|
||||
public enum Connection {
|
||||
case stun
|
||||
@@ -181,6 +186,7 @@ public struct PresentationGroupCallState: Equatable {
|
||||
public var recordingStartTimestamp: Int32?
|
||||
public var title: String?
|
||||
public var raisedHand: Bool
|
||||
public var scheduleTimestamp: Int32?
|
||||
|
||||
public init(
|
||||
myPeerId: PeerId,
|
||||
@@ -191,7 +197,8 @@ public struct PresentationGroupCallState: Equatable {
|
||||
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
||||
recordingStartTimestamp: Int32?,
|
||||
title: String?,
|
||||
raisedHand: Bool
|
||||
raisedHand: Bool,
|
||||
scheduleTimestamp: Int32?
|
||||
) {
|
||||
self.myPeerId = myPeerId
|
||||
self.networkState = networkState
|
||||
@@ -202,6 +209,7 @@ public struct PresentationGroupCallState: Equatable {
|
||||
self.recordingStartTimestamp = recordingStartTimestamp
|
||||
self.title = title
|
||||
self.raisedHand = raisedHand
|
||||
self.scheduleTimestamp = scheduleTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +307,8 @@ public protocol PresentationGroupCall: class {
|
||||
|
||||
var isVideo: Bool { get }
|
||||
|
||||
var schedulePending: Bool { get }
|
||||
|
||||
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
|
||||
|
||||
var canBeRemoved: Signal<Bool, NoError> { get }
|
||||
@@ -313,6 +323,9 @@ public protocol PresentationGroupCall: class {
|
||||
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
|
||||
var reconnectedAsEvents: Signal<Peer, NoError> { get }
|
||||
|
||||
func schedule(timestamp: Int32)
|
||||
func startScheduled()
|
||||
|
||||
func reconnect(with invite: String)
|
||||
func reconnect(as peerId: PeerId)
|
||||
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
||||
@@ -355,4 +368,5 @@ public protocol PresentationCallManager: class {
|
||||
|
||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||
func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
}
|
||||
})]), nil)
|
||||
default:
|
||||
break
|
||||
applyPaymentMethod(method)
|
||||
}
|
||||
} else {
|
||||
applyPaymentMethod(method)
|
||||
|
||||
@@ -120,7 +120,7 @@ private final class TipValueNode: ASDisplayNode {
|
||||
self.action?()
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> CGFloat {
|
||||
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) {
|
||||
var updateBackground = false
|
||||
let backgroundColor = isHighlighted ? UIColor(rgb: 0x00A650) : UIColor(rgb: 0xE5F6ED)
|
||||
if let currentBackgroundColor = self.currentBackgroundColor {
|
||||
@@ -142,20 +142,22 @@ private final class TipValueNode: ASDisplayNode {
|
||||
|
||||
let calculatedWidth = max(titleSize.width + 16.0 * 2.0, minWidth)
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
return (calculatedWidth, { calculatedWidth in
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
let size = CGSize(width: calculatedWidth, height: height)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
let size = CGSize(width: calculatedWidth, height: height)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return size.width
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
let tipMeasurementNode: ImmediateTextNode
|
||||
let tipCurrencyNode: ImmediateTextNode
|
||||
private let textNode: TextFieldNode
|
||||
|
||||
private var formatterDelegate: CurrencyUITextFieldDelegate?
|
||||
@@ -172,6 +174,9 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.tipMeasurementNode = ImmediateTextNode()
|
||||
self.tipCurrencyNode = ImmediateTextNode()
|
||||
|
||||
self.textNode = TextFieldNode()
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
@@ -190,6 +195,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.tipCurrencyNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.textNode.clipsToBounds = true
|
||||
@@ -221,7 +227,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
//TODO:locali
|
||||
//TODO:localize
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Enter Custom", font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
@@ -236,17 +242,6 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((labelsContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - labelLayout.size.width, y: floor((labelsContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
|
||||
let text: String
|
||||
if item.numericValue == 0 {
|
||||
text = ""
|
||||
} else {
|
||||
text = formatCurrencyAmount(item.numericValue, currency: item.currency)
|
||||
}
|
||||
if strongSelf.textNode.textField.text ?? "" != text {
|
||||
strongSelf.textNode.textField.text = text
|
||||
strongSelf.labelNode.isHidden = !text.isEmpty
|
||||
}
|
||||
|
||||
if strongSelf.formatterDelegate == nil {
|
||||
strongSelf.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: item.currency, { formatter in
|
||||
formatter.maxValue = currencyToFractionalAmount(value: item.maxValue, currency: item.currency) ?? 10000.0
|
||||
@@ -275,18 +270,31 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
strongSelf.textNode.textField.keyboardType = .decimalPad
|
||||
strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight))
|
||||
var textInputFrame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight))
|
||||
|
||||
var currencyText: (String, String) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
|
||||
if strongSelf.textNode.textField.text ?? "" != currencyText.0 {
|
||||
strongSelf.textNode.textField.text = currencyText.0
|
||||
strongSelf.labelNode.isHidden = !currencyText.0.isEmpty
|
||||
}
|
||||
|
||||
strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor)
|
||||
let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size)
|
||||
|
||||
strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: " \(currencyText.1)", font: titleFont, textColor: textColor)
|
||||
let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
|
||||
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
|
||||
textInputFrame.origin.x -= currencySize.width
|
||||
|
||||
strongSelf.textNode.frame = textInputFrame
|
||||
|
||||
let valueHeight: CGFloat = 52.0
|
||||
let valueY: CGFloat = labelsContentHeight + 9.0
|
||||
|
||||
var index = 0
|
||||
var variantsOffset: CGFloat = 16.0
|
||||
var variantLayouts: [(CGFloat, (CGFloat) -> Void)] = []
|
||||
var totalMinWidth: CGFloat = 0.0
|
||||
for (variantText, variantValue) in item.availableVariants {
|
||||
if index != 0 {
|
||||
variantsOffset += 12.0
|
||||
}
|
||||
|
||||
let valueNode: TipValueNode
|
||||
if strongSelf.valueNodes.count > index {
|
||||
valueNode = strongSelf.valueNodes[index]
|
||||
@@ -295,18 +303,46 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
strongSelf.valueNodes.append(valueNode)
|
||||
strongSelf.scrollNode.addSubnode(valueNode)
|
||||
}
|
||||
let nodeWidth = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight)
|
||||
let (nodeMinWidth, nodeApply) = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight)
|
||||
valueNode.action = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.item?.updateValue(variantValue)
|
||||
}
|
||||
valueNode.frame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight))
|
||||
variantsOffset += nodeWidth
|
||||
totalMinWidth += nodeMinWidth
|
||||
variantLayouts.append((nodeMinWidth, nodeApply))
|
||||
index += 1
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = params.leftInset + 16.0
|
||||
var scaleFactor: CGFloat = 1.0
|
||||
let availableWidth = params.width - sideInset * 2.0 - CGFloat(max(0, item.availableVariants.count - 1)) * 12.0
|
||||
if totalMinWidth < availableWidth {
|
||||
scaleFactor = availableWidth / totalMinWidth
|
||||
}
|
||||
|
||||
var variantsOffset: CGFloat = sideInset
|
||||
for index in 0 ..< item.availableVariants.count {
|
||||
if index != 0 {
|
||||
variantsOffset += 12.0
|
||||
}
|
||||
|
||||
let valueNode: TipValueNode = strongSelf.valueNodes[index]
|
||||
let (minWidth, nodeApply) = variantLayouts[index]
|
||||
|
||||
let nodeWidth = floor(scaleFactor * minWidth)
|
||||
|
||||
var valueFrame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight))
|
||||
if scaleFactor > 1.0 && index == item.availableVariants.count - 1 {
|
||||
valueFrame.size.width = params.width - sideInset - valueFrame.minX
|
||||
}
|
||||
|
||||
valueNode.frame = valueFrame
|
||||
nodeApply(nodeWidth)
|
||||
variantsOffset += nodeWidth
|
||||
}
|
||||
|
||||
variantsOffset += 16.0
|
||||
|
||||
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY)))
|
||||
@@ -342,7 +378,15 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
if let value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) {
|
||||
if var value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) {
|
||||
if value > item.maxValue {
|
||||
value = item.maxValue
|
||||
|
||||
let currencyText: (String, String) = formatCurrencyAmountCustom(value, currency: item.currency)
|
||||
if self.textNode.textField.text ?? "" != currencyText.0 {
|
||||
self.textNode.textField.text = currencyText.0
|
||||
}
|
||||
}
|
||||
item.updateValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||
}
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
||||
}
|
||||
@@ -552,7 +552,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||
}
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
|
||||
}
|
||||
@@ -958,7 +958,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var hideAuthor = false
|
||||
switch contentPeer {
|
||||
case let .chat(itemPeer):
|
||||
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
||||
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
||||
|
||||
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
|
||||
initialHideAuthor = true
|
||||
|
||||
@@ -46,7 +46,7 @@ private func messageGroupType(messages: [Message]) -> MessageGroupType {
|
||||
return currentType
|
||||
}
|
||||
|
||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) {
|
||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) {
|
||||
let peer: Peer?
|
||||
|
||||
let message = messages.last
|
||||
@@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
}
|
||||
default:
|
||||
hideAuthor = true
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
}
|
||||
}
|
||||
case _ as TelegramMediaExpiredContent:
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
}
|
||||
case let poll as TelegramMediaPoll:
|
||||
|
||||
@@ -569,12 +569,15 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let additionalActionsNode = self.additionalActionsNode else {
|
||||
guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,11 +1561,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if let previousActionsContainerNode = previousActionsContainerNode {
|
||||
if transition.isAnimated {
|
||||
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions {
|
||||
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true {
|
||||
var initialFrame = self.actionsContainerNode.frame
|
||||
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
|
||||
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
|
||||
@@ -1773,6 +1772,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
|
||||
public var dismissed: (() -> Void)?
|
||||
|
||||
public var useComplexItemsTransitionAnimation = false
|
||||
|
||||
private var shouldBeDismissedDisposable: Disposable?
|
||||
|
||||
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) {
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import ReactionSelectionNode
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
|
||||
|
||||
if let fromWindow = fromView.window, let toWindow = toView.window {
|
||||
targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width
|
||||
}
|
||||
return targetWindowFrame
|
||||
}
|
||||
|
||||
final class PinchSourceGesture: UIPinchGestureRecognizer {
|
||||
private final class Target {
|
||||
var updated: (() -> Void)?
|
||||
|
||||
@objc func onGesture(_ gesture: UIPinchGestureRecognizer) {
|
||||
self.updated?()
|
||||
}
|
||||
}
|
||||
|
||||
private let target: Target
|
||||
|
||||
private(set) var currentTransform: (CGFloat, CGPoint)?
|
||||
|
||||
var began: (() -> Void)?
|
||||
var updated: ((CGFloat, CGPoint) -> Void)?
|
||||
var ended: (() -> Void)?
|
||||
|
||||
private var lastLocation: CGPoint?
|
||||
private var currentOffset = CGPoint()
|
||||
|
||||
init() {
|
||||
self.target = Target()
|
||||
|
||||
super.init(target: self.target, action: #selector(self.target.onGesture(_:)))
|
||||
|
||||
self.target.updated = { [weak self] in
|
||||
self?.gestureUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.lastLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if touches.count >= 2 {
|
||||
var locationSum = CGPoint()
|
||||
for touch in touches {
|
||||
let point = touch.location(in: self.view)
|
||||
locationSum.x += point.x
|
||||
locationSum.y += point.y
|
||||
}
|
||||
locationSum.x /= CGFloat(touches.count)
|
||||
locationSum.y /= CGFloat(touches.count)
|
||||
if let lastLocation = self.lastLocation {
|
||||
self.currentOffset = CGPoint(x: locationSum.x - lastLocation.x, y: locationSum.y - lastLocation.y)
|
||||
} else {
|
||||
self.lastLocation = locationSum
|
||||
self.currentOffset = CGPoint()
|
||||
}
|
||||
if let (scale, _) = self.currentTransform {
|
||||
self.currentTransform = (scale, self.currentOffset)
|
||||
self.updated?(scale, self.currentOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func gestureUpdated() {
|
||||
switch self.state {
|
||||
case .began:
|
||||
self.lastLocation = nil
|
||||
self.currentOffset = CGPoint()
|
||||
self.currentTransform = nil
|
||||
self.began?()
|
||||
case .changed:
|
||||
let scale = max(1.0, self.scale)
|
||||
self.currentTransform = (scale, self.currentOffset)
|
||||
self.updated?(scale, self.currentOffset)
|
||||
case .ended, .cancelled:
|
||||
self.ended?()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelContextGestures(node: ASDisplayNode) {
|
||||
if let node = node as? ContextControllerSourceNode {
|
||||
node.cancelGesture()
|
||||
}
|
||||
|
||||
if let supernode = node.supernode {
|
||||
cancelContextGestures(node: supernode)
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for recognizer in gestureRecognizers {
|
||||
if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer {
|
||||
recognizer.cancel()
|
||||
} else if let recognizer = recognizer as? WindowPanRecognizer {
|
||||
recognizer.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let superview = view.superview {
|
||||
cancelContextGestures(view: superview)
|
||||
}
|
||||
}
|
||||
|
||||
public final class PinchSourceContainerNode: ASDisplayNode {
|
||||
public let contentNode: ASDisplayNode
|
||||
public var contentRect: CGRect = CGRect()
|
||||
private(set) var naturalContentFrame: CGRect?
|
||||
|
||||
fileprivate let gesture: PinchSourceGesture
|
||||
|
||||
public var isPinchGestureEnabled: Bool = false {
|
||||
didSet {
|
||||
if self.isPinchGestureEnabled != oldValue {
|
||||
self.gesture.isEnabled = self.isPinchGestureEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isActive: Bool = false
|
||||
|
||||
public var activate: ((PinchSourceContainerNode) -> Void)?
|
||||
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var deactivate: (() -> Void)?
|
||||
var updated: ((CGFloat, CGPoint) -> Void)?
|
||||
|
||||
override public init() {
|
||||
self.gesture = PinchSourceGesture()
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
|
||||
self.gesture.began = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
cancelContextGestures(node: strongSelf)
|
||||
cancelContextGestures(view: strongSelf.view)
|
||||
strongSelf.isActive = true
|
||||
|
||||
strongSelf.activate?(strongSelf)
|
||||
}
|
||||
|
||||
self.gesture.ended = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.isActive = false
|
||||
strongSelf.deactivate?()
|
||||
}
|
||||
|
||||
self.gesture.updated = { [weak self] scale, offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updated?(scale, offset)
|
||||
strongSelf.scaleUpdated?(scale, .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(self.gesture)
|
||||
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
return strongSelf.isActive
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.naturalContentFrame = contentFrame
|
||||
if !self.isActive {
|
||||
transition.updateFrame(node: self.contentNode, frame: contentFrame)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreToNaturalSize() {
|
||||
guard let naturalContentFrame = self.naturalContentFrame else {
|
||||
return
|
||||
}
|
||||
self.contentNode.frame = naturalContentFrame
|
||||
}
|
||||
}
|
||||
|
||||
private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
private weak var controller: PinchController?
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var isAnimatingOut: Bool = false
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
init(controller: PinchController, sourceNode: PinchSourceContainerNode) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.dimNode.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
|
||||
self.sourceNode.deactivate = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
|
||||
self.sourceNode.updated = { [weak self] scale, offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
|
||||
strongSelf.sourceNode.contentNode.transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x / scale, offset.y / scale, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) {
|
||||
if self.isAnimatingOut {
|
||||
return
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let convertedFrame = convertFrame(self.sourceNode.contentNode.frame, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.addSubnode(self.sourceNode.contentNode)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
let performCompletion: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.sourceNode.restoreToNaturalSize()
|
||||
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
if let (scale, offset) = self.sourceNode.gesture.currentTransform {
|
||||
let duration = 0.4
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.prepareImpact(.light)
|
||||
Queue.mainQueue().after(0.2, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.hapticFeedback?.impact(.light)
|
||||
})
|
||||
|
||||
self.sourceNode.scaleUpdated?(1.0, transition)
|
||||
|
||||
self.sourceNode.contentNode.transform = CATransform3DIdentity
|
||||
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
|
||||
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x, y: offset.y), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
|
||||
performCompletion()
|
||||
})
|
||||
|
||||
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
|
||||
} else {
|
||||
performCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PinchController: ViewController, StandalonePresentableController {
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
|
||||
private var wasDismissed = false
|
||||
|
||||
private var controllerNode: PinchControllerNode {
|
||||
return self.displayNode as! PinchControllerNode
|
||||
}
|
||||
|
||||
public init(sourceNode: PinchSourceContainerNode) {
|
||||
self.sourceNode = sourceNode
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.lockOrientation = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self._ready.set(.single(true))
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,12 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U
|
||||
return tintedImage
|
||||
}
|
||||
|
||||
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat]) -> UIImage? {
|
||||
public enum GradientImageDirection {
|
||||
case vertical
|
||||
case horizontal
|
||||
}
|
||||
|
||||
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? {
|
||||
guard colors.count == locations.count else {
|
||||
return nil
|
||||
}
|
||||
@@ -395,7 +400,7 @@ public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [C
|
||||
var locations = locations
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
|
||||
@@ -82,6 +82,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||
self.validatedGesture = false
|
||||
self.currentAllowedDirections = []
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
let touch = touches.first!
|
||||
|
||||
@@ -12,15 +12,15 @@ public protocol TransformImageCustomArguments {
|
||||
}
|
||||
|
||||
public struct TransformImageArguments: Equatable {
|
||||
public let corners: ImageCorners
|
||||
public var corners: ImageCorners
|
||||
|
||||
public let imageSize: CGSize
|
||||
public let boundingSize: CGSize
|
||||
public let intrinsicInsets: UIEdgeInsets
|
||||
public let resizeMode: TransformImageResizeMode
|
||||
public let emptyColor: UIColor?
|
||||
public let custom: TransformImageCustomArguments?
|
||||
public let scale: CGFloat?
|
||||
public var imageSize: CGSize
|
||||
public var boundingSize: CGSize
|
||||
public var intrinsicInsets: UIEdgeInsets
|
||||
public var resizeMode: TransformImageResizeMode
|
||||
public var emptyColor: UIColor?
|
||||
public var custom: TransformImageCustomArguments?
|
||||
public var scale: CGFloat?
|
||||
|
||||
public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor? = nil, custom: TransformImageCustomArguments? = nil, scale: CGFloat? = nil) {
|
||||
self.corners = corners
|
||||
|
||||
@@ -13,6 +13,10 @@ public final class WindowPanRecognizer: UIGestureRecognizer {
|
||||
|
||||
self.previousPoints.removeAll()
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
private func addPoint(_ point: CGPoint) {
|
||||
self.previousPoints.append((point, CACurrentMediaTime()))
|
||||
|
||||
@@ -907,7 +907,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
|
||||
var generalMessageContentKind: MessageContentKind?
|
||||
for message in messages {
|
||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId)
|
||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
||||
if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
|
||||
generalMessageContentKind = currentKind
|
||||
} else {
|
||||
@@ -1056,7 +1056,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
var messageContentKinds = Set<MessageContentKindKey>()
|
||||
|
||||
for message in messages {
|
||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId)
|
||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
||||
if beganContentKindScanning && currentKind != generalMessageContentKind {
|
||||
generalMessageContentKind = nil
|
||||
} else if !beganContentKindScanning || currentKind == generalMessageContentKind {
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageAnchorItem: InstantPageItem {
|
||||
let wantsNode: Bool = false
|
||||
@@ -28,7 +29,7 @@ final class InstantPageAnchorItem: InstantPageItem {
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageArticleItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -35,7 +36,7 @@ final class InstantPageArticleItem: InstantPageItem {
|
||||
self.hasRTL = hasRTL
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageArticleNode(context: context, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, openUrl: openUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageAudioItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -24,7 +25,7 @@ final class InstantPageAudioItem: InstantPageItem {
|
||||
self.medias = [media]
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia)
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,9 @@ final class InstantPageContentNode : ASDisplayNode {
|
||||
self?.openMedia(media)
|
||||
}, longPressMedia: { [weak self] media in
|
||||
self?.longPressMedia(media)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
},
|
||||
activatePinchPreview: nil,
|
||||
openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
}, openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
|
||||
@@ -146,7 +146,7 @@ public final class InstantPageController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = InstantPageControllerNode(context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in
|
||||
self.displayNode = InstantPageControllerNode(controller: self, context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in
|
||||
return self?.navigationController as? NavigationController
|
||||
}, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||
|
||||
@@ -16,8 +16,10 @@ import GalleryUI
|
||||
import OpenInExternalAppUI
|
||||
import LocationUI
|
||||
import UndoUI
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private weak var controller: InstantPageController?
|
||||
private let context: AccountContext
|
||||
private var settings: InstantPagePresentationSettings?
|
||||
private var themeSettings: PresentationThemeSettings?
|
||||
@@ -89,7 +91,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details)
|
||||
}
|
||||
|
||||
init(context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) {
|
||||
init(controller: InstantPageController, context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.presentationTheme = presentationTheme
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
@@ -556,6 +559,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self?.openMedia(media)
|
||||
}, longPressMedia: { [weak self] media in
|
||||
self?.longPressMedia(media)
|
||||
}, activatePinchPreview: { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
let pinchController = PinchController(sourceNode: sourceNode)
|
||||
controller.window?.presentInGlobalOverlay(pinchController)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
}, openUrl: { [weak self] url in
|
||||
|
||||
@@ -8,6 +8,7 @@ import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageDetailsItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -40,7 +41,7 @@ final class InstantPageDetailsItem: InstantPageItem {
|
||||
self.index = index
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
var expanded: Bool?
|
||||
if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] {
|
||||
expanded = currentlyExpanded
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageFeedbackItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -21,7 +22,7 @@ final class InstantPageFeedbackItem: InstantPageItem {
|
||||
self.webPage = webPage
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageFeedbackNode(context: context, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
protocol InstantPageImageAttribute {
|
||||
}
|
||||
@@ -45,8 +46,8 @@ final class InstantPageImageItem: InstantPageItem {
|
||||
self.fit = fit
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia)
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview)
|
||||
}
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool {
|
||||
|
||||
@@ -15,6 +15,7 @@ import LocationResources
|
||||
import LiveLocationPositionNode
|
||||
import AppBundle
|
||||
import TelegramUIPreferences
|
||||
import ContextUI
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: (Bool) -> Void
|
||||
@@ -34,7 +35,8 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
private let longPressMedia: (InstantPageMedia) -> Void
|
||||
|
||||
private var fetchControls: FetchControls?
|
||||
|
||||
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
private let imageNode: TransformImageNode
|
||||
private let statusNode: RadialStatusNode
|
||||
private let linkIconNode: ASImageNode
|
||||
@@ -48,7 +50,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
private var themeUpdated: Bool = false
|
||||
|
||||
init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) {
|
||||
init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.webPage = webPage
|
||||
@@ -59,15 +61,17 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.fit = fit
|
||||
self.openMedia = openMedia
|
||||
self.longPressMedia = longPressMedia
|
||||
|
||||
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
||||
self.linkIconNode = ASImageNode()
|
||||
self.pinNode = ChatMessageLiveLocationPositionNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
|
||||
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
@@ -97,10 +101,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
if media.url != nil {
|
||||
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
|
||||
self.addSubnode(self.linkIconNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.statusNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||
}
|
||||
} else if let file = media.media as? TelegramMediaFile {
|
||||
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||
@@ -114,16 +118,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
}
|
||||
if file.isVideo {
|
||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||
self.addSubnode(self.statusNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||
}
|
||||
} else if let map = media.media as? TelegramMediaMap {
|
||||
self.addSubnode(self.pinNode)
|
||||
|
||||
var zoom: Int32 = 12
|
||||
|
||||
var dimensions = CGSize(width: 200.0, height: 100.0)
|
||||
for attribute in self.attributes {
|
||||
if let mapAttribute = attribute as? InstantPageMapAttribute {
|
||||
zoom = mapAttribute.zoom
|
||||
dimensions = mapAttribute.dimensions
|
||||
break
|
||||
}
|
||||
@@ -135,7 +137,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||
self.addSubnode(self.statusNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
if let activatePinchPreview = activatePinchPreview {
|
||||
self.pinchContainerNode.activate = { sourceNode in
|
||||
activatePinchPreview(sourceNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +206,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
if self.currentSize != size || self.themeUpdated {
|
||||
self.currentSize = size
|
||||
self.themeUpdated = false
|
||||
|
||||
|
||||
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.pinchContainerNode.update(size: size, transition: .immediate)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let radialStatusSize: CGFloat = 50.0
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
protocol InstantPageItem {
|
||||
var frame: CGRect { get set }
|
||||
@@ -16,7 +17,7 @@ protocol InstantPageItem {
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool
|
||||
func drawInTile(context: CGContext)
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)?
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)?
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPagePeerReferenceItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -27,7 +28,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem {
|
||||
self.rtl = rtl
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPagePeerReferenceNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPagePlayableVideoItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -29,7 +30,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem {
|
||||
self.interactive = interactive
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPagePlayableVideoNode(context: context, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
enum InstantPageShape {
|
||||
case rect
|
||||
@@ -62,7 +63,7 @@ final class InstantPageShapeItem: InstantPageItem {
|
||||
return false
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageSlideshowItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -21,7 +22,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
|
||||
self.medias = medias
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageSlideshowNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
|
||||
let media = self.items[index]
|
||||
let contentNode: ASDisplayNode
|
||||
if let _ = media.media as? TelegramMediaImage {
|
||||
contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia)
|
||||
contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: nil)
|
||||
} else if let file = media.media as? TelegramMediaFile {
|
||||
contentNode = ASDisplayNode()
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
private struct TableSide: OptionSet {
|
||||
var rawValue: Int32 = 0
|
||||
@@ -200,12 +201,12 @@ final class InstantPageTableItem: InstantPageScrollableItem {
|
||||
return false
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
var additionalNodes: [InstantPageNode] = []
|
||||
for cell in self.cells {
|
||||
for item in cell.additionalItems {
|
||||
if item.wantsNode {
|
||||
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
|
||||
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
|
||||
node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY)
|
||||
additionalNodes.append(node)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
public final class InstantPageUrlItem: Equatable {
|
||||
public let url: String
|
||||
@@ -436,7 +437,7 @@ final class InstantPageTextItem: InstantPageItem {
|
||||
return false
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -485,11 +486,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem {
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
var additionalNodes: [InstantPageNode] = []
|
||||
for item in additionalItems {
|
||||
if item.wantsNode {
|
||||
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
|
||||
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
|
||||
node.frame = item.frame
|
||||
additionalNodes.append(node)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class InstantPageWebEmbedItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
@@ -25,7 +26,7 @@ final class InstantPageWebEmbedItem: InstantPageItem {
|
||||
self.enableScrolling = enableScrolling
|
||||
}
|
||||
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
|
||||
return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight)
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,12 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.imageNode.transform = CATransform3DMakeScale(self.scale, self.scale, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
public init(size: CGSize) {
|
||||
self.intrinsicSize = size
|
||||
|
||||
@@ -286,4 +292,11 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
open override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.imageNode.bounds = self.bounds
|
||||
self.imageNode.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +565,7 @@ public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReferenc
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad)
|
||||
|> map { _, _, generate in
|
||||
return generate
|
||||
@@ -684,7 +684,7 @@ public func chatMessagePhotoInternal(photoData: Signal<Tuple4<Data?, Data?, Chat
|
||||
return context
|
||||
}
|
||||
|
||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||
let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true)
|
||||
|
||||
context.withFlippedContext { c in
|
||||
c.setBlendMode(.copy)
|
||||
|
||||
@@ -135,19 +135,21 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text: String
|
||||
var actions: [TextAlertAction] = [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
||||
]
|
||||
var actions: [TextAlertAction] = []
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = presentationData.strings.Login_CodeFloodError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
case .invalidPhoneNumber:
|
||||
text = presentationData.strings.Login_InvalidPhoneError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
case .phoneNumberOccupied:
|
||||
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
case .phoneBanned:
|
||||
text = presentationData.strings.Login_PhoneBannedError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
|
||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@@ -162,6 +164,7 @@ final class ChangePhoneNumberController: ViewController, MFMailComposeViewContro
|
||||
}))
|
||||
case .generic:
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
}
|
||||
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
|
||||
|
||||
@@ -3,8 +3,6 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private let textFont: UIFont = Font.regular(16.0)
|
||||
|
||||
public final class SolidRoundedButtonTheme {
|
||||
public let backgroundColor: UIColor
|
||||
public let foregroundColor: UIColor
|
||||
|
||||
@@ -241,7 +241,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
|
||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
|
||||
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||
text = foldLineBreaks(text)
|
||||
|
||||
@@ -288,7 +289,6 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
@@ -333,7 +333,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 {
|
||||
panelData = nil
|
||||
} else {
|
||||
panelData = currentGroupCall != nil || availableState?.participantCount == 0 ? nil : availableState
|
||||
panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil) ? nil : availableState
|
||||
}
|
||||
|
||||
let wasEmpty = strongSelf.groupCallPanelData == nil
|
||||
@@ -406,7 +406,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
strongSelf.joinGroupCall(
|
||||
peerId: groupCallPanelData.peerId,
|
||||
invite: nil,
|
||||
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title)
|
||||
activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribed: false)
|
||||
)
|
||||
})
|
||||
if let navigationBar = self.navigationBar {
|
||||
|
||||
@@ -41,6 +41,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
case accept
|
||||
case end
|
||||
case cancel
|
||||
case share
|
||||
}
|
||||
|
||||
var appearance: Appearance
|
||||
@@ -254,6 +255,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
|
||||
context.strokePath()
|
||||
})
|
||||
case .share:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: imageColor)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
|
||||
@@ -15,11 +15,20 @@ private let blue = UIColor(rgb: 0x0078ff)
|
||||
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
||||
private let green = UIColor(rgb: 0x33c659)
|
||||
private let activeBlue = UIColor(rgb: 0x00a0b9)
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
private let pink = UIColor(rgb: 0xef436c)
|
||||
|
||||
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||
enum State {
|
||||
case connecting
|
||||
case cantSpeak
|
||||
case active
|
||||
case speaking
|
||||
}
|
||||
private let foregroundView: UIView
|
||||
private let foregroundGradientLayer: CAGradientLayer
|
||||
private let maskCurveView: VoiceCurveView
|
||||
private let initialTimestamp = CACurrentMediaTime()
|
||||
|
||||
var audioLevel: Float = 0.0 {
|
||||
didSet {
|
||||
@@ -35,9 +44,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
var speaking: Bool? = nil {
|
||||
var state: State = .connecting {
|
||||
didSet {
|
||||
if self.speaking != oldValue {
|
||||
if self.state != oldValue {
|
||||
self.updateGradientColors()
|
||||
}
|
||||
}
|
||||
@@ -46,13 +55,26 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||
private func updateGradientColors() {
|
||||
let initialColors = self.foregroundGradientLayer.colors
|
||||
let targetColors: [CGColor]
|
||||
if let speaking = self.speaking {
|
||||
targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor]
|
||||
} else {
|
||||
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
|
||||
switch self.state {
|
||||
case .connecting:
|
||||
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
|
||||
case .active:
|
||||
targetColors = [blue.cgColor, lightBlue.cgColor]
|
||||
case .speaking:
|
||||
targetColors = [green.cgColor, activeBlue.cgColor]
|
||||
case .cantSpeak:
|
||||
targetColors = [purple.cgColor, pink.cgColor]
|
||||
}
|
||||
|
||||
if CACurrentMediaTime() - self.initialTimestamp > 0.1 {
|
||||
self.foregroundGradientLayer.colors = targetColors
|
||||
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.foregroundGradientLayer.colors = targetColors
|
||||
CATransaction.commit()
|
||||
}
|
||||
self.foregroundGradientLayer.colors = targetColors
|
||||
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||
}
|
||||
|
||||
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||
@@ -177,6 +199,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
private var currentCallState: PresentationCallState?
|
||||
private var currentGroupCallState: PresentationGroupCallSummaryState?
|
||||
private var currentIsMuted = true
|
||||
private var currentCantSpeak = false
|
||||
private var currentMembers: PresentationGroupCallMembers?
|
||||
private var currentIsConnected = true
|
||||
|
||||
@@ -279,16 +302,24 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
strongSelf.currentMembers = members
|
||||
|
||||
var isMuted = isMuted
|
||||
var cantSpeak = false
|
||||
if let state = state, let muteState = state.callState.muteState {
|
||||
if !muteState.canUnmute {
|
||||
isMuted = true
|
||||
cantSpeak = true
|
||||
}
|
||||
}
|
||||
if state?.callState.scheduleTimestamp != nil {
|
||||
cantSpeak = true
|
||||
}
|
||||
strongSelf.currentIsMuted = isMuted
|
||||
strongSelf.currentCantSpeak = cantSpeak
|
||||
|
||||
let currentIsConnected: Bool
|
||||
if let state = state, case .connected = state.callState.networkState {
|
||||
currentIsConnected = true
|
||||
} else if state?.callState.scheduleTimestamp != nil {
|
||||
currentIsConnected = true
|
||||
} else {
|
||||
currentIsConnected = false
|
||||
}
|
||||
@@ -439,7 +470,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize)
|
||||
}
|
||||
|
||||
self.backgroundNode.speaking = self.currentIsConnected ? !self.currentIsMuted : nil
|
||||
let state: CallStatusBarBackgroundNode.State
|
||||
if self.currentIsConnected {
|
||||
if self.currentCantSpeak {
|
||||
state = .cantSpeak
|
||||
} else if self.currentIsMuted {
|
||||
state = .active
|
||||
} else {
|
||||
state = .speaking
|
||||
}
|
||||
} else {
|
||||
state = .connecting
|
||||
}
|
||||
self.backgroundNode.state = state
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,29 @@ import SyncCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import SwiftSignalKit
|
||||
import AnimatedAvatarSetNode
|
||||
import AudioBlob
|
||||
|
||||
func textForTimeout(value: Int32) -> String {
|
||||
if value < 3600 {
|
||||
let minutes = value / 60
|
||||
let seconds = value % 60
|
||||
let secondsPadding = seconds < 10 ? "0" : ""
|
||||
return "\(minutes):\(secondsPadding)\(seconds)"
|
||||
} else {
|
||||
let hours = value / 3600
|
||||
let minutes = (value % 3600) / 60
|
||||
let minutesPadding = minutes < 10 ? "0" : ""
|
||||
let seconds = value % 60
|
||||
let secondsPadding = seconds < 10 ? "0" : ""
|
||||
return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.semibold(15.0)
|
||||
private let subtitleFont = Font.regular(13.0)
|
||||
|
||||
@@ -79,6 +96,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
private var dateTimeFormat: PresentationDateTimeFormat
|
||||
|
||||
private let tapAction: () -> Void
|
||||
|
||||
@@ -102,6 +120,10 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
private var textIsActive = false
|
||||
private let muteIconNode: ASImageNode
|
||||
|
||||
private var isScheduled = false
|
||||
private var currentText: String = ""
|
||||
private var updateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private let avatarsContext: AnimatedAvatarSetContext
|
||||
private var avatarsContent: AnimatedAvatarSetContext.Content?
|
||||
private let avatarsNode: AnimatedAvatarSetNode
|
||||
@@ -125,6 +147,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.context = context
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||
|
||||
self.tapAction = tapAction
|
||||
|
||||
@@ -135,6 +158,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.joinButton = HighlightableButtonNode()
|
||||
self.joinButtonTitleNode = ImmediateTextNode()
|
||||
self.joinButtonBackgroundNode = ASImageNode()
|
||||
self.joinButtonBackgroundNode.clipsToBounds = true
|
||||
self.joinButtonBackgroundNode.displaysAsynchronously = false
|
||||
self.joinButtonBackgroundNode.cornerRadius = 14.0
|
||||
|
||||
self.micButton = HighlightTrackingButtonNode()
|
||||
self.micButtonForegroundNode = VoiceChatMicrophoneNode()
|
||||
@@ -198,6 +224,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.membersDisposable.dispose()
|
||||
self.isMutedDisposable.dispose()
|
||||
self.audioLevelGeneratorTimer?.invalidate()
|
||||
self.updateTimer?.invalidate()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
@@ -250,6 +277,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
public func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||
|
||||
self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
@@ -257,18 +285,31 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
|
||||
self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor
|
||||
|
||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor)
|
||||
|
||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: self.joinButtonTitleNode.attributedText?.string ?? "", font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor)
|
||||
|
||||
self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
|
||||
|
||||
self.updateJoinButton()
|
||||
|
||||
if let (size, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateJoinButton() {
|
||||
if self.isScheduled {
|
||||
let purple = UIColor(rgb: 0x3252ef)
|
||||
let pink = UIColor(rgb: 0xef436c)
|
||||
self.joinButtonBackgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 1.0), colors: [purple, pink], locations: [0.0, 1.0], direction: .horizontal)
|
||||
self.joinButtonBackgroundNode.backgroundColor = nil
|
||||
} else {
|
||||
self.joinButtonBackgroundNode.image = nil
|
||||
self.joinButtonBackgroundNode.backgroundColor = self.theme.chat.inputPanel.actionControlFillColor
|
||||
}
|
||||
}
|
||||
|
||||
private func animateTextChange() {
|
||||
if let snapshotView = self.textNode.view.snapshotContentTree() {
|
||||
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
|
||||
@@ -298,6 +339,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
} else {
|
||||
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
||||
}
|
||||
self.currentText = membersText
|
||||
|
||||
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
||||
|
||||
@@ -321,9 +363,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
} else {
|
||||
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
|
||||
}
|
||||
|
||||
strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
|
||||
|
||||
strongSelf.currentText = membersText
|
||||
|
||||
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
|
||||
|
||||
if let (size, leftInset, rightInset) = strongSelf.validLayout {
|
||||
@@ -382,7 +423,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
strongSelf.micButton.view.insertSubview(audioLevelView, at: 0)
|
||||
}
|
||||
|
||||
let level = min(1.0, max(0.0, CGFloat(value)))
|
||||
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
||||
if value > 0.0 {
|
||||
strongSelf.audioLevelView?.startAnimating()
|
||||
@@ -400,9 +440,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
} else {
|
||||
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
||||
}
|
||||
self.currentText = membersText
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
|
||||
|
||||
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
|
||||
|
||||
updateAudioLevels = true
|
||||
@@ -466,6 +505,57 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
|
||||
}
|
||||
|
||||
var joinText = self.strings.VoiceChat_PanelJoin.uppercased()
|
||||
var title = self.strings.VoiceChat_Title
|
||||
var text = self.currentText
|
||||
var isScheduled = false
|
||||
if let scheduleTime = self.currentData?.info.scheduleTimestamp {
|
||||
isScheduled = true
|
||||
let timeString = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime)
|
||||
if let voiceChatTitle = self.currentData?.info.title {
|
||||
title = voiceChatTitle
|
||||
text = self.strings.Conversation_ScheduledVoiceChatStartsOn(timeString).0
|
||||
} else {
|
||||
title = self.strings.Conversation_ScheduledVoiceChat
|
||||
text = self.strings.Conversation_ScheduledVoiceChatStartsOnShort(timeString).0
|
||||
}
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let elapsedTime = scheduleTime - currentTime
|
||||
if elapsedTime >= 86400 {
|
||||
joinText = timeIntervalString(strings: strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
joinText = "+\(textForTimeout(value: abs(elapsedTime)))"
|
||||
} else {
|
||||
joinText = textForTimeout(value: elapsedTime)
|
||||
}
|
||||
|
||||
if self.updateTimer == nil {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self, let (size, leftInset, rightInset) = strongSelf.validLayout {
|
||||
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.updateTimer = timer
|
||||
timer.start()
|
||||
}
|
||||
} else {
|
||||
if let timer = self.updateTimer {
|
||||
self.updateTimer = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
|
||||
title = voiceChatTitle
|
||||
}
|
||||
}
|
||||
|
||||
if self.isScheduled != isScheduled {
|
||||
self.isScheduled = isScheduled
|
||||
self.updateJoinButton()
|
||||
}
|
||||
|
||||
self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
|
||||
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
||||
let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0)
|
||||
let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize)
|
||||
@@ -500,15 +590,17 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
self.micButtonBackgroundNode.image = updatedImage
|
||||
}
|
||||
}
|
||||
|
||||
var title = self.strings.VoiceChat_Title
|
||||
if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 {
|
||||
title = voiceChatTitle
|
||||
}
|
||||
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width / 2.0 - 56.0, height: .greatestFiniteMagnitude))
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor)
|
||||
|
||||
var constrainedWidth = size.width / 2.0 - 56.0
|
||||
if isScheduled {
|
||||
constrainedWidth = size.width - 100.0
|
||||
}
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize)
|
||||
|
||||
@@ -624,6 +624,113 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
}
|
||||
}
|
||||
|
||||
private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
|
||||
let (presentationData, present, openSettings) = self.getDeviceAccessData()
|
||||
|
||||
let isVideo = false
|
||||
|
||||
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
|
||||
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
if isVideo && value {
|
||||
DeviceAccess.authorizeAccess(to: .camera(.videoCall), presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|
||||
return accessEnabledSignal
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { [weak self] accessEnabled -> Signal<Bool, NoError> in
|
||||
guard let strongSelf = self else {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
if !accessEnabled {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
audioSession: strongSelf.audioSession,
|
||||
callKitIntegration: nil,
|
||||
getDeviceAccessData: strongSelf.getDeviceAccessData,
|
||||
initialCall: nil,
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
invite: nil,
|
||||
joinAsPeerId: nil
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(true)
|
||||
strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||
guard let strongSelf = self, let call = call else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if strongSelf.currentGroupCall === call {
|
||||
strongSelf.updateCurrentGroupCall(nil)
|
||||
strongSelf.currentGroupCallPromise.set(.single(nil))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
return .single(true)
|
||||
}
|
||||
}
|
||||
|
||||
public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult {
|
||||
let begin: () -> Void = { [weak self] in
|
||||
let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start()
|
||||
}
|
||||
|
||||
if let currentGroupCall = self.currentGroupCallValue {
|
||||
if endCurrentIfAny {
|
||||
let endSignal = currentGroupCall.leave(terminateIfPossible: false)
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
self.startCallDisposable.set(endSignal.start(next: { _ in
|
||||
begin()
|
||||
}))
|
||||
} else {
|
||||
return .alreadyInProgress(currentGroupCall.peerId)
|
||||
}
|
||||
} else if let currentCall = self.currentCall {
|
||||
if endCurrentIfAny {
|
||||
self.callKitIntegration?.dropCall(uuid: currentCall.internalId)
|
||||
self.startCallDisposable.set((currentCall.hangUp()
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
begin()
|
||||
}))
|
||||
} else {
|
||||
return .alreadyInProgress(currentCall.peerId)
|
||||
}
|
||||
} else {
|
||||
begin()
|
||||
}
|
||||
return .success
|
||||
}
|
||||
|
||||
public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult {
|
||||
let begin: () -> Void = { [weak self] in
|
||||
if let requestJoinAsPeerId = requestJoinAsPeerId {
|
||||
|
||||
@@ -77,6 +77,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
clientParams: nil,
|
||||
streamDcId: nil,
|
||||
title: call.title,
|
||||
scheduleTimestamp: call.scheduleTimestamp,
|
||||
recordingStartTimestamp: nil,
|
||||
sortAscending: true
|
||||
),
|
||||
@@ -120,7 +121,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
}
|
||||
return GroupCallPanelData(
|
||||
peerId: peerId,
|
||||
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, recordingStartTimestamp: nil, sortAscending: state.sortAscending),
|
||||
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, scheduleTimestamp: state.scheduleTimestamp, recordingStartTimestamp: nil, sortAscending: state.sortAscending),
|
||||
topParticipants: topParticipants,
|
||||
participantCount: state.totalCount,
|
||||
activeSpeakers: activeSpeakers,
|
||||
@@ -205,7 +206,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach
|
||||
}
|
||||
|
||||
private extension PresentationGroupCallState {
|
||||
static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState {
|
||||
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?) -> PresentationGroupCallState {
|
||||
return PresentationGroupCallState(
|
||||
myPeerId: myPeerId,
|
||||
networkState: .connecting,
|
||||
@@ -215,7 +216,8 @@ private extension PresentationGroupCallState {
|
||||
defaultParticipantMuteState: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: title,
|
||||
raisedHand: false
|
||||
raisedHand: false,
|
||||
scheduleTimestamp: scheduleTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -508,6 +510,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
private let joinDisposable = MetaDisposable()
|
||||
private let requestDisposable = MetaDisposable()
|
||||
private let startDisposable = MetaDisposable()
|
||||
private var groupCallParticipantUpdatesDisposable: Disposable?
|
||||
|
||||
private let networkStateDisposable = MetaDisposable()
|
||||
@@ -550,6 +553,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
private var peerUpdatesSubscription: Disposable?
|
||||
|
||||
public private(set) var schedulePending = false
|
||||
|
||||
init(
|
||||
accountContext: AccountContext,
|
||||
audioSession: ManagedAudioSession,
|
||||
@@ -572,8 +577,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.peerId = peerId
|
||||
self.invite = invite
|
||||
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
|
||||
self.schedulePending = initialCall == nil
|
||||
|
||||
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title)
|
||||
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp)
|
||||
self.statePromise = ValuePromise(self.stateValue)
|
||||
|
||||
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
@@ -761,7 +767,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
})
|
||||
|
||||
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
|
||||
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title))
|
||||
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title, scheduleTimestamp: initialCall.scheduleTimestamp, subscribed: initialCall.subscribed))
|
||||
}) {
|
||||
self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId)
|
||||
} else {
|
||||
@@ -805,7 +811,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
strongSelf.stateValue = updatedValue
|
||||
})
|
||||
|
||||
self.requestCall(movingFromBroadcastToRtc: false)
|
||||
if let _ = self.initialCall {
|
||||
self.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -815,6 +823,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.audioSessionDisposable?.dispose()
|
||||
self.joinDisposable.dispose()
|
||||
self.requestDisposable.dispose()
|
||||
self.startDisposable.dispose()
|
||||
self.groupCallParticipantUpdatesDisposable?.dispose()
|
||||
self.leaveDisposable.dispose()
|
||||
self.isMutedDisposable.dispose()
|
||||
@@ -1039,287 +1048,301 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
|
||||
var shouldJoin = false
|
||||
let activeCallInfo: GroupCallInfo?
|
||||
switch previousInternalState {
|
||||
case .active:
|
||||
break
|
||||
default:
|
||||
if case let .active(callInfo) = internalState {
|
||||
let callContext: OngoingGroupCallContext
|
||||
if let current = self.callContext {
|
||||
callContext = current
|
||||
case let .active(previousCallInfo):
|
||||
if case let .active(callInfo) = internalState {
|
||||
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
|
||||
activeCallInfo = callInfo
|
||||
} else {
|
||||
var outgoingAudioBitrateKbit: Int32?
|
||||
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
|
||||
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 {
|
||||
outgoingAudioBitrateKbit = value
|
||||
}
|
||||
activeCallInfo = nil
|
||||
}
|
||||
default:
|
||||
if case let .active(callInfo) = internalState {
|
||||
shouldJoin = callInfo.scheduleTimestamp == nil
|
||||
activeCallInfo = callInfo
|
||||
} else {
|
||||
activeCallInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
if shouldJoin, let callInfo = activeCallInfo {
|
||||
let callContext: OngoingGroupCallContext
|
||||
if let current = self.callContext {
|
||||
callContext = current
|
||||
} else {
|
||||
var outgoingAudioBitrateKbit: Int32?
|
||||
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
|
||||
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 {
|
||||
outgoingAudioBitrateKbit = value
|
||||
}
|
||||
|
||||
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
|
||||
}
|
||||
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .established = strongSelf.internalState {
|
||||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
}
|
||||
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
|
||||
self.incomingVideoSourcePromise.set(callContext.videoSources
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] sources -> [PeerId: UInt32] in
|
||||
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return [:]
|
||||
return
|
||||
}
|
||||
var result: [PeerId: UInt32] = [:]
|
||||
for source in sources {
|
||||
if let peerId = strongSelf.ssrcMapping[source] {
|
||||
result[peerId] = source
|
||||
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
|
||||
}
|
||||
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .established = strongSelf.internalState {
|
||||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
}
|
||||
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
|
||||
self.incomingVideoSourcePromise.set(callContext.videoSources
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] sources -> [PeerId: UInt32] in
|
||||
guard let strongSelf = self else {
|
||||
return [:]
|
||||
}
|
||||
var result: [PeerId: UInt32] = [:]
|
||||
for source in sources {
|
||||
if let peerId = strongSelf.ssrcMapping[source] {
|
||||
result[peerId] = source
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
self.callContext = callContext
|
||||
}
|
||||
self.joinDisposable.set((callContext.joinPayload
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
if lhs.0 != rhs.0 {
|
||||
return false
|
||||
}
|
||||
if lhs.1 != rhs.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerAdminIds: Signal<[PeerId], NoError>
|
||||
let peerId = strongSelf.peerId
|
||||
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
peerAdminIds = Signal { subscriber in
|
||||
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in
|
||||
var peerIds = Set<PeerId>()
|
||||
for item in list.list {
|
||||
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
||||
peerIds.insert(item.peer.id)
|
||||
}
|
||||
}
|
||||
subscriber.putNext(Array(peerIds))
|
||||
})
|
||||
return disposable
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> runOn(.mainQueue())
|
||||
} else {
|
||||
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
||||
var result: [PeerId] = []
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||
if let participants = cachedData.participants {
|
||||
for participant in participants.participants {
|
||||
if case .creator = participant {
|
||||
result.append(participant.peerId)
|
||||
} else if case .admin = participant {
|
||||
result.append(participant.peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
self.callContext = callContext
|
||||
}
|
||||
}
|
||||
self.joinDisposable.set((callContext.joinPayload
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
if lhs.0 != rhs.0 {
|
||||
return false
|
||||
}
|
||||
if lhs.1 != rhs.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
|
||||
|
||||
strongSelf.currentLocalSsrc = ssrc
|
||||
strongSelf.requestDisposable.set((joinGroupCall(
|
||||
account: strongSelf.account,
|
||||
peerId: strongSelf.peerId,
|
||||
joinAs: strongSelf.joinAsPeerId,
|
||||
callId: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
preferMuted: true,
|
||||
joinPayload: joinPayload,
|
||||
peerAdminIds: peerAdminIds,
|
||||
inviteHash: strongSelf.invite
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerAdminIds: Signal<[PeerId], NoError>
|
||||
let peerId = strongSelf.peerId
|
||||
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
peerAdminIds = Signal { subscriber in
|
||||
let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in
|
||||
var peerIds = Set<PeerId>()
|
||||
for item in list.list {
|
||||
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
||||
peerIds.insert(item.peer.id)
|
||||
}
|
||||
if let clientParams = joinCallResult.callInfo.clientParams {
|
||||
strongSelf.ssrcMapping.removeAll()
|
||||
let addedParticipants: [(UInt32, String?)] = []
|
||||
for participant in joinCallResult.state.participants {
|
||||
if let ssrc = participant.ssrc {
|
||||
strongSelf.ssrcMapping[ssrc] = participant.peer.id
|
||||
//addedParticipants.append((participant.ssrc, participant.jsonParams))
|
||||
}
|
||||
}
|
||||
|
||||
switch joinCallResult.connectionMode {
|
||||
case .rtc:
|
||||
strongSelf.currentConnectionMode = .rtc
|
||||
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
||||
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
|
||||
case .broadcast:
|
||||
strongSelf.currentConnectionMode = .broadcast
|
||||
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
|
||||
}
|
||||
|
||||
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
|
||||
}
|
||||
}, error: { error in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .anonymousNotAllowed = error {
|
||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .tooManyParticipants = error {
|
||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .invalidJoinAsPeer = error {
|
||||
let peerId = strongSelf.peerId
|
||||
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
|
||||
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedChannelData {
|
||||
return current.withUpdatedCallJoinPeerId(nil)
|
||||
} else if let current = current as? CachedGroupData {
|
||||
return current.withUpdatedCallJoinPeerId(nil)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
subscriber.putNext(Array(peerIds))
|
||||
})
|
||||
return disposable
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> runOn(.mainQueue())
|
||||
} else {
|
||||
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
||||
var result: [PeerId] = []
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||
if let participants = cachedData.participants {
|
||||
for participant in participants.participants {
|
||||
if case .creator = participant {
|
||||
result.append(participant.peerId)
|
||||
} else if case .admin = participant {
|
||||
result.append(participant.peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
strongSelf.currentLocalSsrc = ssrc
|
||||
strongSelf.requestDisposable.set((joinGroupCall(
|
||||
account: strongSelf.account,
|
||||
peerId: strongSelf.peerId,
|
||||
joinAs: strongSelf.joinAsPeerId,
|
||||
callId: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
preferMuted: true,
|
||||
joinPayload: joinPayload,
|
||||
peerAdminIds: peerAdminIds,
|
||||
inviteHash: strongSelf.invite
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let clientParams = joinCallResult.callInfo.clientParams {
|
||||
strongSelf.ssrcMapping.removeAll()
|
||||
let addedParticipants: [(UInt32, String?)] = []
|
||||
for participant in joinCallResult.state.participants {
|
||||
if let ssrc = participant.ssrc {
|
||||
strongSelf.ssrcMapping[ssrc] = participant.peer.id
|
||||
//addedParticipants.append((participant.ssrc, participant.jsonParams))
|
||||
}
|
||||
}
|
||||
|
||||
switch joinCallResult.connectionMode {
|
||||
case .rtc:
|
||||
strongSelf.currentConnectionMode = .rtc
|
||||
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
||||
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
|
||||
case .broadcast:
|
||||
strongSelf.currentConnectionMode = .broadcast
|
||||
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
|
||||
}
|
||||
|
||||
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
|
||||
}
|
||||
}, error: { error in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .anonymousNotAllowed = error {
|
||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .tooManyParticipants = error {
|
||||
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .invalidJoinAsPeer = error {
|
||||
let peerId = strongSelf.peerId
|
||||
let _ = clearCachedGroupCallDisplayAsAvailablePeers(account: strongSelf.accountContext.account, peerId: peerId).start()
|
||||
let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedChannelData {
|
||||
return current.withUpdatedCallJoinPeerId(nil)
|
||||
} else if let current = current as? CachedGroupData {
|
||||
return current.withUpdatedCallJoinPeerId(nil)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
})
|
||||
}).start()
|
||||
}
|
||||
strongSelf.markAsCanBeRemoved()
|
||||
}))
|
||||
strongSelf.markAsCanBeRemoved()
|
||||
}))
|
||||
}))
|
||||
|
||||
self.networkStateDisposable.set((callContext.networkState
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let mappedState: PresentationGroupCallState.NetworkState
|
||||
if state.isConnected {
|
||||
mappedState = .connected
|
||||
} else {
|
||||
mappedState = .connecting
|
||||
}
|
||||
|
||||
let wasConnecting = strongSelf.stateValue.networkState == .connecting
|
||||
if strongSelf.stateValue.networkState != mappedState {
|
||||
strongSelf.stateValue.networkState = mappedState
|
||||
}
|
||||
let isConnecting = mappedState == .connecting
|
||||
|
||||
self.networkStateDisposable.set((callContext.networkState
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let mappedState: PresentationGroupCallState.NetworkState
|
||||
if state.isConnected {
|
||||
mappedState = .connected
|
||||
} else {
|
||||
mappedState = .connecting
|
||||
}
|
||||
|
||||
let wasConnecting = strongSelf.stateValue.networkState == .connecting
|
||||
if strongSelf.stateValue.networkState != mappedState {
|
||||
strongSelf.stateValue.networkState = mappedState
|
||||
}
|
||||
let isConnecting = mappedState == .connecting
|
||||
|
||||
if strongSelf.isCurrentlyConnecting != isConnecting {
|
||||
strongSelf.isCurrentlyConnecting = isConnecting
|
||||
if isConnecting {
|
||||
strongSelf.startCheckingCallIfNeeded()
|
||||
} else {
|
||||
strongSelf.checkCallDisposable?.dispose()
|
||||
strongSelf.checkCallDisposable = nil
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
|
||||
|
||||
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
|
||||
if isConnecting {
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
} else {
|
||||
strongSelf.toneRenderer = nil
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.isCurrentlyConnecting != isConnecting {
|
||||
strongSelf.isCurrentlyConnecting = isConnecting
|
||||
if isConnecting {
|
||||
strongSelf.didStartConnectingOnce = true
|
||||
strongSelf.startCheckingCallIfNeeded()
|
||||
} else {
|
||||
strongSelf.checkCallDisposable?.dispose()
|
||||
strongSelf.checkCallDisposable = nil
|
||||
}
|
||||
|
||||
if state.isConnected {
|
||||
if !strongSelf.didConnectOnce {
|
||||
strongSelf.didConnectOnce = true
|
||||
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = strongSelf.reconnectingAsPeer {
|
||||
strongSelf.reconnectingAsPeer = nil
|
||||
strongSelf.reconnectedAsEventsPipe.putNext(peer)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.isNoiseSuppressionEnabledDisposable.set((callContext.isNoiseSuppressionEnabled
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
|
||||
}))
|
||||
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
|
||||
|
||||
self.audioLevelsDisposable.set((callContext.audioLevels
|
||||
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
|
||||
if isConnecting {
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
} else {
|
||||
strongSelf.toneRenderer = nil
|
||||
}
|
||||
var result: [(PeerId, UInt32, Float, Bool)] = []
|
||||
var myLevel: Float = 0.0
|
||||
var myLevelHasVoice: Bool = false
|
||||
var missingSsrcs = Set<UInt32>()
|
||||
for (ssrcKey, level, hasVoice) in levels {
|
||||
var peerId: PeerId?
|
||||
let ssrcValue: UInt32
|
||||
switch ssrcKey {
|
||||
case .local:
|
||||
peerId = strongSelf.joinAsPeerId
|
||||
ssrcValue = 0
|
||||
case let .source(ssrc):
|
||||
peerId = strongSelf.ssrcMapping[ssrc]
|
||||
ssrcValue = ssrc
|
||||
}
|
||||
if let peerId = peerId {
|
||||
if case .local = ssrcKey {
|
||||
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
||||
myLevel = level
|
||||
myLevelHasVoice = hasVoice
|
||||
}
|
||||
}
|
||||
|
||||
if isConnecting {
|
||||
strongSelf.didStartConnectingOnce = true
|
||||
}
|
||||
|
||||
if state.isConnected {
|
||||
if !strongSelf.didConnectOnce {
|
||||
strongSelf.didConnectOnce = true
|
||||
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
}
|
||||
|
||||
if let peer = strongSelf.reconnectingAsPeer {
|
||||
strongSelf.reconnectingAsPeer = nil
|
||||
strongSelf.reconnectedAsEventsPipe.putNext(peer)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.isNoiseSuppressionEnabledDisposable.set((callContext.isNoiseSuppressionEnabled
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isNoiseSuppressionEnabledPromise.set(value)
|
||||
}))
|
||||
|
||||
self.audioLevelsDisposable.set((callContext.audioLevels
|
||||
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var result: [(PeerId, UInt32, Float, Bool)] = []
|
||||
var myLevel: Float = 0.0
|
||||
var myLevelHasVoice: Bool = false
|
||||
var missingSsrcs = Set<UInt32>()
|
||||
for (ssrcKey, level, hasVoice) in levels {
|
||||
var peerId: PeerId?
|
||||
let ssrcValue: UInt32
|
||||
switch ssrcKey {
|
||||
case .local:
|
||||
peerId = strongSelf.joinAsPeerId
|
||||
ssrcValue = 0
|
||||
case let .source(ssrc):
|
||||
peerId = strongSelf.ssrcMapping[ssrc]
|
||||
ssrcValue = ssrc
|
||||
}
|
||||
if let peerId = peerId {
|
||||
if case .local = ssrcKey {
|
||||
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
||||
myLevel = level
|
||||
myLevelHasVoice = hasVoice
|
||||
}
|
||||
result.append((peerId, ssrcValue, level, hasVoice))
|
||||
} else if ssrcValue != 0 {
|
||||
missingSsrcs.insert(ssrcValue)
|
||||
}
|
||||
result.append((peerId, ssrcValue, level, hasVoice))
|
||||
} else if ssrcValue != 0 {
|
||||
missingSsrcs.insert(ssrcValue)
|
||||
}
|
||||
|
||||
strongSelf.speakingParticipantsContext.update(levels: result)
|
||||
|
||||
let mappedLevel = myLevel * 1.5
|
||||
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
||||
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
|
||||
|
||||
if !missingSsrcs.isEmpty {
|
||||
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.speakingParticipantsContext.update(levels: result)
|
||||
|
||||
let mappedLevel = myLevel * 1.5
|
||||
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
||||
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
|
||||
|
||||
if !missingSsrcs.isEmpty {
|
||||
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
switch previousInternalState {
|
||||
@@ -1339,6 +1362,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
if self.stateValue.title != initialState.title {
|
||||
self.stateValue.title = initialState.title
|
||||
}
|
||||
if self.stateValue.scheduleTimestamp != initialState.scheduleTimestamp {
|
||||
self.stateValue.scheduleTimestamp = initialState.scheduleTimestamp
|
||||
}
|
||||
|
||||
let accountContext = self.accountContext
|
||||
let peerId = self.peerId
|
||||
@@ -1630,6 +1656,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
|
||||
strongSelf.stateValue.title = state.title
|
||||
strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp
|
||||
|
||||
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
|
||||
id: callInfo.id,
|
||||
@@ -1638,6 +1665,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
clientParams: nil,
|
||||
streamDcId: nil,
|
||||
title: state.title,
|
||||
scheduleTimestamp: state.scheduleTimestamp,
|
||||
recordingStartTimestamp: state.recordingStartTimestamp,
|
||||
sortAscending: state.sortAscending
|
||||
))))
|
||||
@@ -1887,7 +1915,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
|
||||
self.leaving = true
|
||||
if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc {
|
||||
if let callInfo = self.internalState.callInfo {
|
||||
if terminateIfPossible {
|
||||
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
@@ -1896,7 +1924,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.markAsCanBeRemoved()
|
||||
}))
|
||||
} else {
|
||||
} else if let localSsrc = self.currentLocalSsrc {
|
||||
if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl {
|
||||
let account = self.account
|
||||
let id = callInfo.id
|
||||
@@ -1907,6 +1935,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
self.markAsCanBeRemoved()
|
||||
} else {
|
||||
self.markAsCanBeRemoved()
|
||||
}
|
||||
} else {
|
||||
self.markAsCanBeRemoved()
|
||||
@@ -1957,6 +1987,39 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.callContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
|
||||
}
|
||||
|
||||
public func schedule(timestamp: Int32) {
|
||||
guard self.schedulePending else {
|
||||
return
|
||||
}
|
||||
|
||||
self.schedulePending = false
|
||||
self.stateValue.scheduleTimestamp = timestamp
|
||||
|
||||
self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||
}))
|
||||
}
|
||||
|
||||
public func startScheduled() {
|
||||
guard case let .active(callInfo) = self.internalState else {
|
||||
return
|
||||
}
|
||||
|
||||
self.stateValue.scheduleTimestamp = nil
|
||||
|
||||
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||
}))
|
||||
}
|
||||
|
||||
public func raiseHand() {
|
||||
guard let membersValue = self.membersValue else {
|
||||
return
|
||||
@@ -2207,7 +2270,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
if let value = value {
|
||||
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title)
|
||||
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: nil, subscribed: false)
|
||||
|
||||
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
|
||||
} else {
|
||||
@@ -2217,7 +2280,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
public func invitePeer(_ peerId: PeerId) -> Bool {
|
||||
guard case let .established(callInfo, _, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else {
|
||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2236,11 +2299,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.invitedPeersValue = updatedInvitedPeers
|
||||
}
|
||||
|
||||
public func updateTitle(_ title: String){
|
||||
guard case let .established(callInfo, _, _, _, _) = self.internalState else {
|
||||
public func updateTitle(_ title: String) {
|
||||
guard let callInfo = self.internalState.callInfo else {
|
||||
return
|
||||
}
|
||||
|
||||
self.stateValue.title = title
|
||||
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ private let blobSize = CGSize(width: 190.0, height: 190.0)
|
||||
private let smallScale: CGFloat = 0.48
|
||||
private let smallIconScale: CGFloat = 0.69
|
||||
|
||||
private let buttonHeight: CGFloat = 52.0
|
||||
|
||||
final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
enum State: Equatable {
|
||||
enum ActiveState: Equatable {
|
||||
@@ -34,7 +36,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
case muted
|
||||
case on
|
||||
}
|
||||
|
||||
enum ScheduledState: Equatable {
|
||||
case start
|
||||
case subscribe
|
||||
case unsubscribe
|
||||
}
|
||||
|
||||
case button(text: String)
|
||||
case scheduled(state: ScheduledState)
|
||||
case connecting
|
||||
case active(state: ActiveState)
|
||||
}
|
||||
@@ -53,6 +63,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
private let iconNode: VoiceChatActionButtonIconNode
|
||||
private let titleLabel: ImmediateTextNode
|
||||
private let subtitleLabel: ImmediateTextNode
|
||||
private let buttonTitleLabel: ImmediateTextNode
|
||||
|
||||
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)?
|
||||
|
||||
@@ -103,7 +114,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .connecting:
|
||||
case .connecting, .button, .scheduled:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@@ -121,12 +132,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
|
||||
init() {
|
||||
self.bottomNode = ASDisplayNode()
|
||||
self.bottomNode.isUserInteractionEnabled = false
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.isUserInteractionEnabled = false
|
||||
self.backgroundNode = VoiceChatActionButtonBackgroundNode()
|
||||
self.iconNode = VoiceChatActionButtonIconNode(isColored: false)
|
||||
|
||||
self.titleLabel = ImmediateTextNode()
|
||||
self.subtitleLabel = ImmediateTextNode()
|
||||
self.buttonTitleLabel = ImmediateTextNode()
|
||||
self.buttonTitleLabel.isUserInteractionEnabled = false
|
||||
self.buttonTitleLabel.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -138,26 +154,38 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
self.containerNode.addSubnode(self.backgroundNode)
|
||||
self.containerNode.addSubnode(self.iconNode)
|
||||
|
||||
self.containerNode.addSubnode(self.buttonTitleLabel)
|
||||
|
||||
self.highligthedChanged = { [weak self] pressing in
|
||||
if let strongSelf = self {
|
||||
guard let (_, _, _, _, small, _, _, snap) = strongSelf.currentParams else {
|
||||
guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else {
|
||||
return
|
||||
}
|
||||
if pressing {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
if small {
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9)
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
|
||||
if case .button = state {
|
||||
strongSelf.containerNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.containerNode.alpha = 0.4
|
||||
} else {
|
||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
if small {
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9)
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale * 0.9)
|
||||
} else {
|
||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
|
||||
}
|
||||
}
|
||||
} else if !strongSelf.pressing {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
if small {
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale)
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
|
||||
if case .button = state {
|
||||
strongSelf.containerNode.alpha = 1.0
|
||||
strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
if small {
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale)
|
||||
transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallIconScale)
|
||||
} else {
|
||||
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +242,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||
let totalHeight = titleSize.height + subtitleSize.height + 1.0
|
||||
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 70.0), size: titleSize)
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 88.0), size: titleSize)
|
||||
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
|
||||
|
||||
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
@@ -232,7 +260,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .connecting:
|
||||
case .connecting, .button, .scheduled:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -271,6 +299,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
|
||||
let icon: VoiceChatActionButtonIconAnimationState
|
||||
switch state {
|
||||
case .button:
|
||||
icon = .empty
|
||||
case let .scheduled(state):
|
||||
switch state {
|
||||
case .start:
|
||||
icon = .start
|
||||
case .subscribe:
|
||||
icon = .subscribe
|
||||
case .unsubscribe:
|
||||
icon = .unsubscribe
|
||||
}
|
||||
case let .active(state):
|
||||
switch state {
|
||||
case .on:
|
||||
@@ -290,7 +329,6 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
self.previousIcon = icon
|
||||
|
||||
self.iconNode.enqueueState(icon)
|
||||
// self.iconNode.update(state: VoiceChatMicrophoneNode.State(muted: iconMuted, filled: true, color: iconColor), animated: true)
|
||||
}
|
||||
|
||||
func update(snap: Bool, animated: Bool) {
|
||||
@@ -312,8 +350,26 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
|
||||
self.statePromise.set(state)
|
||||
|
||||
if let previousState = previousState, case .button = previousState, case .scheduled = state {
|
||||
self.buttonTitleLabel.alpha = 0.0
|
||||
self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24)
|
||||
|
||||
self.iconNode.alpha = 1.0
|
||||
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
}
|
||||
|
||||
var backgroundState: VoiceChatActionButtonBackgroundNode.State
|
||||
switch state {
|
||||
case let .button(text):
|
||||
backgroundState = .button
|
||||
self.buttonTitleLabel.alpha = 1.0
|
||||
self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white)
|
||||
let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0))
|
||||
self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
case .scheduled:
|
||||
backgroundState = .disabled
|
||||
case let .active(state):
|
||||
switch state {
|
||||
case .on:
|
||||
@@ -340,14 +396,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
applyParams(animated: animated)
|
||||
self.applyParams(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
var hitRect = self.bounds
|
||||
if let (_, buttonSize, _, _, _, _, _, _) = self.currentParams {
|
||||
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
|
||||
if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams {
|
||||
if case .button = state {
|
||||
hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight)
|
||||
} else {
|
||||
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
|
||||
}
|
||||
}
|
||||
let result = super.hitTest(point, with: event)
|
||||
if !hitRect.contains(point) {
|
||||
@@ -453,6 +513,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
enum State: Equatable {
|
||||
case connecting
|
||||
case disabled
|
||||
case button
|
||||
case blob(Bool)
|
||||
}
|
||||
|
||||
@@ -546,7 +607,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
self.maskProgressLayer.lineCap = .round
|
||||
self.maskProgressLayer.path = path
|
||||
|
||||
let largerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width + progressLineWidth, height: buttonSize.height + progressLineWidth))).cgPath
|
||||
let circleFrame = CGRect(origin: CGPoint(x: (358 - buttonSize.width) / 2.0, y: (358 - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
|
||||
|
||||
self.maskCircleLayer.fillColor = white.cgColor
|
||||
self.maskCircleLayer.path = largerCirclePath
|
||||
self.maskCircleLayer.isHidden = true
|
||||
@@ -825,7 +888,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
self.maskBlobView.startAnimating()
|
||||
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
}
|
||||
|
||||
|
||||
private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) {
|
||||
CATransaction.begin()
|
||||
let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0)
|
||||
@@ -872,7 +935,8 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
|
||||
self.updateGlowAndGradientAnimations(type: type, previousType: nil)
|
||||
|
||||
if case .blob = self.state {
|
||||
if case .connecting = self.state {
|
||||
} else {
|
||||
self.maskBlobView.isHidden = false
|
||||
self.maskBlobView.startAnimating()
|
||||
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
@@ -907,6 +971,47 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
private func setupButtonAnimation() {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.backgroundCircleLayer.isHidden = true
|
||||
self.foregroundCircleLayer.isHidden = true
|
||||
self.maskCircleLayer.isHidden = false
|
||||
self.maskProgressLayer.isHidden = true
|
||||
self.maskGradientLayer.isHidden = true
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight), cornerRadius: 10.0).cgPath
|
||||
self.maskCircleLayer.path = path
|
||||
|
||||
CATransaction.commit()
|
||||
|
||||
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil)
|
||||
|
||||
self.updatedActive?(true)
|
||||
}
|
||||
|
||||
private func playScheduledAnimation() {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.maskGradientLayer.isHidden = false
|
||||
CATransaction.commit()
|
||||
|
||||
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
|
||||
|
||||
let previousPath = self.maskCircleLayer.path
|
||||
self.maskCircleLayer.path = largerCirclePath
|
||||
|
||||
self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.42, initialVelocity: 0.0, damping: 104.0)
|
||||
|
||||
self.maskBlobView.isHidden = false
|
||||
self.maskBlobView.startAnimating()
|
||||
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
|
||||
let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? 0.8)
|
||||
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
}
|
||||
|
||||
var isActive = false
|
||||
func updateAnimations() {
|
||||
if !self.isCurrentlyInHierarchy {
|
||||
@@ -959,7 +1064,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
self.isActive = false
|
||||
|
||||
if let transition = self.transition {
|
||||
if case .connecting = transition {
|
||||
if case .button = transition {
|
||||
self.playScheduledAnimation()
|
||||
} else if case .connecting = transition {
|
||||
self.playConnectionAnimation(type: .muted) { [weak self] in
|
||||
self?.isActive = false
|
||||
}
|
||||
@@ -969,7 +1076,10 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
self.transition = nil
|
||||
}
|
||||
break
|
||||
case .button:
|
||||
self.updatedActive?(true)
|
||||
self.isActive = false
|
||||
self.setupButtonAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,20 +1147,24 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height)
|
||||
let center = bounds.center
|
||||
|
||||
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize)
|
||||
self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize)
|
||||
|
||||
let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize)
|
||||
self.backgroundCircleLayer.frame = circleFrame
|
||||
self.foregroundCircleLayer.position = center
|
||||
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
|
||||
self.growingForegroundCircleLayer.position = center
|
||||
self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds
|
||||
self.maskCircleLayer.frame = circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||
self.maskCircleLayer.frame = self.bounds
|
||||
// circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
|
||||
self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0)
|
||||
self.foregroundView.frame = self.bounds
|
||||
self.foregroundGradientLayer.frame = self.bounds
|
||||
self.maskGradientLayer.position = center
|
||||
self.maskGradientLayer.bounds = self.bounds
|
||||
self.maskGradientLayer.bounds = bounds
|
||||
self.maskView.frame = self.bounds
|
||||
}
|
||||
}
|
||||
@@ -1386,6 +1500,10 @@ final class BlobView: UIView {
|
||||
}
|
||||
|
||||
enum VoiceChatActionButtonIconAnimationState: Equatable {
|
||||
case empty
|
||||
case start
|
||||
case subscribe
|
||||
case unsubscribe
|
||||
case unmute
|
||||
case mute
|
||||
case hand
|
||||
@@ -1399,6 +1517,7 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
||||
self.isColored = isColored
|
||||
super.init(size: CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
self.scale = 0.8
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
|
||||
}
|
||||
|
||||
@@ -1410,30 +1529,73 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
||||
let previousState = self.iconState
|
||||
self.iconState = state
|
||||
|
||||
if state != .empty {
|
||||
self.alpha = 1.0
|
||||
}
|
||||
switch previousState {
|
||||
case .empty:
|
||||
switch state {
|
||||
case .start:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .subscribe:
|
||||
switch state {
|
||||
case .unsubscribe:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
case .mute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
case .hand:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .unsubscribe:
|
||||
switch state {
|
||||
case .subscribe:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
case .mute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
case .hand:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .start:
|
||||
switch state {
|
||||
case .mute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart")))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .unmute:
|
||||
switch state {
|
||||
case .mute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute")))
|
||||
case .hand:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2")))
|
||||
case .unmute:
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .mute:
|
||||
switch state {
|
||||
case .start:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001))
|
||||
case .unmute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 12), duration: 0.2))
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute")))
|
||||
case .hand:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff")))
|
||||
case .mute:
|
||||
case .empty:
|
||||
self.alpha = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .hand:
|
||||
switch state {
|
||||
case .mute, .unmute:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn")))
|
||||
case .hand:
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import TelegramVoip
|
||||
import TelegramAudio
|
||||
import AccountContext
|
||||
@@ -29,6 +30,7 @@ import LegacyComponents
|
||||
import LegacyMediaPickerUI
|
||||
import WebSearchUI
|
||||
import MapResourceToAvatarSizes
|
||||
import SolidRoundedButtonNode
|
||||
|
||||
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
|
||||
@@ -65,105 +67,6 @@ private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
|
||||
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
|
||||
}
|
||||
|
||||
|
||||
private final class VoiceChatControllerTitleNode: ASDisplayNode {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let infoNode: ASTextNode
|
||||
fileprivate let recordingIconNode: VoiceChatRecordingIconNode
|
||||
|
||||
public var isRecording: Bool = false {
|
||||
didSet {
|
||||
self.recordingIconNode.isHidden = !self.isRecording
|
||||
}
|
||||
}
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.truncationMode = .byTruncatingTail
|
||||
self.titleNode.isOpaque = false
|
||||
|
||||
self.infoNode = ASTextNode()
|
||||
self.infoNode.displaysAsynchronously = false
|
||||
self.infoNode.maximumNumberOfLines = 1
|
||||
self.infoNode.truncationMode = .byTruncatingTail
|
||||
self.infoNode.isOpaque = false
|
||||
|
||||
self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.infoNode)
|
||||
self.addSubnode(self.recordingIconNode)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if point.y > 0.0 && point.y < self.frame.size.height && point.x > min(self.titleNode.frame.minX, self.infoNode.frame.minX) && point.x < max(self.recordingIconNode.frame.maxX, self.infoNode.frame.maxX) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tap() {
|
||||
self.tapped?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) {
|
||||
var titleUpdated = false
|
||||
if let previousTitle = self.titleNode.attributedText?.string {
|
||||
titleUpdated = previousTitle != title
|
||||
}
|
||||
|
||||
if titleUpdated, let snapshotView = self.titleNode.view.snapshotContentTree() {
|
||||
snapshotView.frame = self.titleNode.frame
|
||||
self.view.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: UIColor(rgb: 0xffffff))
|
||||
self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.5))
|
||||
|
||||
let constrainedSize = CGSize(width: size.width - 140.0, height: size.height)
|
||||
let titleSize = self.titleNode.measure(constrainedSize)
|
||||
let infoSize = self.infoNode.measure(constrainedSize)
|
||||
let titleInfoSpacing: CGFloat = 0.0
|
||||
|
||||
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize)
|
||||
|
||||
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
||||
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
|
||||
self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize)
|
||||
}
|
||||
}
|
||||
|
||||
final class GroupVideoNode: ASDisplayNode {
|
||||
private let videoViewContainer: UIView
|
||||
private let videoView: PresentationCallVideoView
|
||||
@@ -730,7 +633,15 @@ public final class VoiceChatController: ViewController {
|
||||
private let leftBorderNode: ASDisplayNode
|
||||
private let rightBorderNode: ASDisplayNode
|
||||
|
||||
private let titleNode: VoiceChatControllerTitleNode
|
||||
private var isScheduling = false
|
||||
private let timerNode: VoiceChatTimerNode
|
||||
private var pickerView: UIDatePicker?
|
||||
private let dateFormatter: DateFormatter
|
||||
private let scheduleTextNode: ImmediateTextNode
|
||||
private let scheduleCancelButton: SolidRoundedButtonNode
|
||||
private var scheduleButtonTitle = ""
|
||||
|
||||
private let titleNode: VoiceChatTitleNode
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
private var floatingHeaderOffset: CGFloat?
|
||||
@@ -823,6 +734,8 @@ public final class VoiceChatController: ViewController {
|
||||
self.context = call.accountContext
|
||||
self.call = call
|
||||
|
||||
self.isScheduling = call.schedulePending
|
||||
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationData = presentationData
|
||||
|
||||
@@ -836,7 +749,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.contentContainer.isHidden = true
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
|
||||
self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor
|
||||
self.backgroundNode.clipsToBounds = false
|
||||
|
||||
if sharedContext.immediateExperimentalUISettings.demoVideoChats {
|
||||
@@ -844,6 +757,8 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.alpha = self.isScheduling ? 0.0 : 1.0
|
||||
self.listNode.isUserInteractionEnabled = !self.isScheduling
|
||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
||||
self.listNode.clipsToBounds = true
|
||||
self.listNode.scroller.bounces = false
|
||||
@@ -870,7 +785,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.closeButton = VoiceChatHeaderButton(context: self.context)
|
||||
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
|
||||
|
||||
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
|
||||
self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme)
|
||||
|
||||
self.topCornersNode = ASImageNode()
|
||||
self.topCornersNode.displaysAsynchronously = false
|
||||
@@ -895,6 +810,13 @@ public final class VoiceChatController: ViewController {
|
||||
self.switchCameraButton.isUserInteractionEnabled = false
|
||||
self.leaveButton = CallControllerButtonItemNode()
|
||||
self.actionButton = VoiceChatActionButton()
|
||||
|
||||
if self.isScheduling {
|
||||
self.audioButton.alpha = 0.0
|
||||
self.audioButton.isUserInteractionEnabled = false
|
||||
self.leaveButton.alpha = 0.0
|
||||
self.leaveButton.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
self.leftBorderNode = ASDisplayNode()
|
||||
self.leftBorderNode.backgroundColor = panelBackgroundColor
|
||||
@@ -906,6 +828,19 @@ public final class VoiceChatController: ViewController {
|
||||
self.rightBorderNode.isUserInteractionEnabled = false
|
||||
self.rightBorderNode.clipsToBounds = false
|
||||
|
||||
self.scheduleTextNode = ImmediateTextNode()
|
||||
self.scheduleTextNode.isHidden = !self.isScheduling
|
||||
|
||||
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
|
||||
self.scheduleCancelButton.isHidden = !self.isScheduling
|
||||
|
||||
self.dateFormatter = DateFormatter()
|
||||
self.dateFormatter.timeStyle = .none
|
||||
self.dateFormatter.dateStyle = .short
|
||||
self.dateFormatter.timeZone = TimeZone.current
|
||||
|
||||
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
|
||||
super.init()
|
||||
|
||||
let statePromise = ValuePromise(State(), ignoreRepeated: true)
|
||||
@@ -1514,6 +1449,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture)
|
||||
contextController.useComplexItemsTransitionAnimation = true
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}, setPeerIdWithRevealedOptions: { peerId, _ in
|
||||
updateState { state in
|
||||
@@ -1550,6 +1486,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
self.bottomPanelNode.addSubnode(self.leaveButton)
|
||||
self.bottomPanelNode.addSubnode(self.actionButton)
|
||||
self.bottomPanelNode.addSubnode(self.scheduleCancelButton)
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.contentContainer)
|
||||
@@ -1563,6 +1500,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.contentContainer.addSubnode(self.leftBorderNode)
|
||||
self.contentContainer.addSubnode(self.rightBorderNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelNode)
|
||||
self.contentContainer.addSubnode(self.timerNode)
|
||||
|
||||
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|
||||
|> mapToSignal { ids -> Signal<[Peer], NoError> in
|
||||
@@ -1619,7 +1557,13 @@ public final class VoiceChatController: ViewController {
|
||||
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
|
||||
strongSelf.currentSubtitle = subtitle
|
||||
|
||||
if let callState = strongSelf.callState, callState.canManageCall {
|
||||
if strongSelf.isScheduling {
|
||||
strongSelf.optionsButtonIsAvatar = false
|
||||
strongSelf.optionsButton.isUserInteractionEnabled = false
|
||||
strongSelf.optionsButton.alpha = 0.0
|
||||
strongSelf.closeButton.isUserInteractionEnabled = false
|
||||
strongSelf.closeButton.alpha = 0.0
|
||||
} else if let callState = strongSelf.callState, callState.canManageCall {
|
||||
strongSelf.optionsButtonIsAvatar = false
|
||||
strongSelf.optionsButton.isUserInteractionEnabled = true
|
||||
strongSelf.optionsButton.alpha = 1.0
|
||||
@@ -1774,16 +1718,6 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// self.memberEventsDisposable.set((self.call.memberEvents
|
||||
// |> deliverOnMainQueue).start(next: { [weak self] event in
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// if event.joined {
|
||||
// strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
|
||||
// }
|
||||
// }))
|
||||
|
||||
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
@@ -1874,22 +1808,32 @@ public final class VoiceChatController: ViewController {
|
||||
}))
|
||||
|
||||
self.titleNode.tapped = { [weak self] in
|
||||
if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden {
|
||||
var hasTooltipAlready = false
|
||||
strongSelf.controller?.forEachController { controller -> Bool in
|
||||
if controller is TooltipScreen {
|
||||
hasTooltipAlready = true
|
||||
if let strongSelf = self {
|
||||
if strongSelf.callState?.canManageCall ?? false {
|
||||
strongSelf.openTitleEditing()
|
||||
} else if !strongSelf.titleNode.recordingIconNode.isHidden {
|
||||
var hasTooltipAlready = false
|
||||
strongSelf.controller?.forEachController { controller -> Bool in
|
||||
if controller is TooltipScreen {
|
||||
hasTooltipAlready = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !hasTooltipAlready {
|
||||
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
|
||||
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: true)
|
||||
}), in: .window(.root))
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !hasTooltipAlready {
|
||||
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
|
||||
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: true)
|
||||
}), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.scheduleCancelButton.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismissScheduled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -1931,7 +1875,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||
|
||||
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get())
|
||||
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get())
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
|
||||
@@ -1965,15 +1909,7 @@ public final class VoiceChatController: ViewController {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title {
|
||||
strongSelf.call.updateTitle(title)
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
strongSelf.openTitleEditing()
|
||||
})))
|
||||
|
||||
var hasPermissions = true
|
||||
@@ -1994,16 +1930,7 @@ public final class VoiceChatController: ViewController {
|
||||
c.setItems(strongSelf.contextMenuPermissionItems())
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
c.setItems(strongSelf.contextMenuPermissionItems())
|
||||
})))
|
||||
|
||||
|
||||
if let inviteLinks = inviteLinks {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||
@@ -2044,25 +1971,27 @@ public final class VoiceChatController: ViewController {
|
||||
self?.controller?.present(alertController, in: .window(.root))
|
||||
}), false))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
if strongSelf.callState?.scheduleTimestamp == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title {
|
||||
strongSelf.call.setShouldBeRecording(true, title: title)
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
||||
strongSelf.call.playTone(.recordingStarted)
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
})
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
})))
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title {
|
||||
strongSelf.call.setShouldBeRecording(true, title: title)
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
||||
strongSelf.call.playTone(.recordingStarted)
|
||||
}
|
||||
})
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
|
||||
@@ -2275,6 +2204,161 @@ public final class VoiceChatController: ViewController {
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
|
||||
if self.isScheduling {
|
||||
self.setupPickerView()
|
||||
self.updateScheduleButtonTitle()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMinimumDate() {
|
||||
let timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = timeZone
|
||||
let currentDate = Date()
|
||||
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
|
||||
components.second = 0
|
||||
let minute = (components.minute ?? 0) % 5
|
||||
|
||||
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!)
|
||||
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!)
|
||||
|
||||
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
|
||||
self.pickerView?.maximumDate = date
|
||||
}
|
||||
|
||||
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate {
|
||||
self.pickerView?.minimumDate = next1MinDate
|
||||
self.pickerView?.date = next5MinDate
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPickerView() {
|
||||
var currentDate: Date?
|
||||
if let pickerView = self.pickerView {
|
||||
currentDate = pickerView.date
|
||||
pickerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
let textColor = UIColor.white
|
||||
UILabel.setDateLabel(textColor)
|
||||
|
||||
let pickerView = UIDatePicker()
|
||||
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
pickerView.datePickerMode = .countDownTimer
|
||||
pickerView.datePickerMode = .dateAndTime
|
||||
pickerView.locale = Locale.current
|
||||
pickerView.timeZone = TimeZone.current
|
||||
pickerView.minuteInterval = 1
|
||||
self.contentContainer.view.addSubview(pickerView)
|
||||
pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
|
||||
if #available(iOS 13.4, *) {
|
||||
pickerView.preferredDatePickerStyle = .wheels
|
||||
}
|
||||
pickerView.setValue(textColor, forKey: "textColor")
|
||||
self.pickerView = pickerView
|
||||
|
||||
self.updateMinimumDate()
|
||||
if let currentDate = currentDate {
|
||||
pickerView.date = currentDate
|
||||
}
|
||||
}
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
private func updateScheduleButtonTitle() {
|
||||
guard let date = self.pickerView?.date else {
|
||||
return
|
||||
}
|
||||
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
let buttonTitle: String
|
||||
if calendar.isDateInToday(date) {
|
||||
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
|
||||
} else if calendar.isDateInTomorrow(date) {
|
||||
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).0
|
||||
} else {
|
||||
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).0
|
||||
}
|
||||
self.scheduleButtonTitle = buttonTitle
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func datePickerUpdated() {
|
||||
self.updateScheduleButtonTitle()
|
||||
}
|
||||
|
||||
private func schedule() {
|
||||
if let date = self.pickerView?.date, date > Date() {
|
||||
self.call.schedule(timestamp: Int32(date.timeIntervalSince1970))
|
||||
|
||||
self.isScheduling = false
|
||||
self.transitionToScheduled()
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissScheduled() {
|
||||
self.leaveDisposable.set((self.call.leave(terminateIfPossible: true)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.controller?.dismiss(closing: true)
|
||||
}))
|
||||
}
|
||||
|
||||
private func transitionToScheduled() {
|
||||
self.optionsButton.alpha = 1.0
|
||||
self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
self.optionsButton.isUserInteractionEnabled = true
|
||||
|
||||
self.closeButton.alpha = 1.0
|
||||
self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
self.closeButton.isUserInteractionEnabled = true
|
||||
|
||||
self.audioButton.alpha = 1.0
|
||||
self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
self.audioButton.isUserInteractionEnabled = true
|
||||
|
||||
self.leaveButton.alpha = 1.0
|
||||
self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
self.leaveButton.isUserInteractionEnabled = true
|
||||
|
||||
self.scheduleCancelButton.alpha = 0.0
|
||||
self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||
|
||||
if let pickerView = self.pickerView {
|
||||
pickerView.alpha = 0.0
|
||||
pickerView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
pickerView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
self.timerNode.alpha = 1.0
|
||||
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.timerNode.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.3, damping: 104.0)
|
||||
self.timerNode.animateIn()
|
||||
|
||||
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
private func transitionToCall() {
|
||||
self.updateIsFullscreen(false, force: true)
|
||||
|
||||
self.listNode.alpha = 1.0
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
self.timerNode.alpha = 0.0
|
||||
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
@objc private func optionsPressed() {
|
||||
@@ -2491,7 +2575,31 @@ public final class VoiceChatController: ViewController {
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
if case .connecting = callState.networkState {
|
||||
if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling {
|
||||
return
|
||||
}
|
||||
if callState.scheduleTimestamp != nil || self.isScheduling {
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.actionButton.pressing = true
|
||||
self.hapticFeedback.impact(.light)
|
||||
case .ended, .cancelled:
|
||||
self.actionButton.pressing = false
|
||||
|
||||
let location = gestureRecognizer.location(in: self.actionButton.view)
|
||||
if self.actionButton.hitTest(location, with: nil) != nil {
|
||||
if self.isScheduling {
|
||||
self.schedule()
|
||||
} else if callState.canManageCall {
|
||||
self.call.startScheduled()
|
||||
self.transitionToCall()
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
if let muteState = callState.muteState {
|
||||
@@ -2548,11 +2656,27 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressed() {
|
||||
if self.isScheduling {
|
||||
self.schedule()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func audioOutputPressed() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
if let _ = self.callState?.scheduleTimestamp {
|
||||
let _ = (self.inviteLinksPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
|
||||
if let inviteLinks = inviteLinks {
|
||||
self?.presentShare(inviteLinks)
|
||||
} else {
|
||||
self?.presentShare(GroupCallInviteLinks(listenerLink: "a", speakerLink: nil))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
||||
return
|
||||
}
|
||||
@@ -2743,8 +2867,8 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
var isFullscreen = false
|
||||
func updateIsFullscreen(_ isFullscreen: Bool) {
|
||||
guard self.isFullscreen != isFullscreen, let (layout, _) = self.validLayout else {
|
||||
func updateIsFullscreen(_ isFullscreen: Bool, force: Bool = false) {
|
||||
guard self.isFullscreen != isFullscreen || force, let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.isFullscreen = isFullscreen
|
||||
@@ -2770,16 +2894,20 @@ public final class VoiceChatController: ViewController {
|
||||
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
|
||||
}
|
||||
|
||||
var isScheduled = false
|
||||
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||
isScheduled = true
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
|
||||
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
|
||||
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
|
||||
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen || isScheduled ? panelBackgroundColor : secondaryPanelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
|
||||
|
||||
if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
|
||||
snapshotView.frame = self.topCornersNode.frame
|
||||
@@ -2814,22 +2942,39 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
var title = self.currentTitle
|
||||
if !self.isFullscreen && !self.currentTitleIsCustom {
|
||||
if self.isScheduling {
|
||||
title = self.presentationData.strings.ScheduleVoiceChat_Title
|
||||
} else if !self.isFullscreen && !self.currentTitleIsCustom {
|
||||
if let navigationController = self.controller?.navigationController as? NavigationController {
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId {
|
||||
title = self.presentationData.strings.VoiceChat_Title
|
||||
if self.callState?.scheduleTimestamp != nil {
|
||||
title = self.presentationData.strings.VoiceChat_ScheduledTitle
|
||||
} else {
|
||||
title = self.presentationData.strings.VoiceChat_Title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle = self.currentSubtitle
|
||||
if self.isScheduling {
|
||||
subtitle = ""
|
||||
} else if self.callState?.scheduleTimestamp != nil {
|
||||
if self.callState?.canManageCall ?? false {
|
||||
subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle
|
||||
} else {
|
||||
subtitle = ""
|
||||
}
|
||||
}
|
||||
|
||||
var size = layout.size
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
size.width = floor(min(size.width, size.height) * 0.5)
|
||||
}
|
||||
|
||||
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: self.currentSubtitle, transition: transition)
|
||||
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: subtitle, transition: transition)
|
||||
}
|
||||
|
||||
private func updateButtons(animated: Bool) {
|
||||
@@ -2866,7 +3011,7 @@ public final class VoiceChatController: ViewController {
|
||||
coloredButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
|
||||
}
|
||||
|
||||
let soundImage: CallControllerButtonItemNode.Content.Image
|
||||
var soundImage: CallControllerButtonItemNode.Content.Image
|
||||
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = coloredButtonAppearance
|
||||
var soundTitle: String = self.presentationData.strings.Call_Speaker
|
||||
switch audioMode {
|
||||
@@ -2890,6 +3035,12 @@ public final class VoiceChatController: ViewController {
|
||||
soundTitle = self.presentationData.strings.Call_Audio
|
||||
}
|
||||
|
||||
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||
soundImage = .share
|
||||
soundTitle = self.presentationData.strings.VoiceChat_ShareShort
|
||||
soundAppearance = coloredButtonAppearance
|
||||
}
|
||||
|
||||
let videoButtonSize: CGSize
|
||||
var buttonsTitleAlpha: CGFloat
|
||||
switch self.displayMode {
|
||||
@@ -2916,6 +3067,7 @@ public final class VoiceChatController: ViewController {
|
||||
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
|
||||
}
|
||||
|
||||
private var ignoreNextConnecting = false
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
@@ -2993,7 +3145,16 @@ public final class VoiceChatController: ViewController {
|
||||
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
|
||||
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
|
||||
|
||||
let centralButtonSize = CGSize(width: 300.0, height: 300.0)
|
||||
if let pickerView = self.pickerView {
|
||||
transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0))
|
||||
}
|
||||
|
||||
let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)
|
||||
transition.updateFrame(node: self.timerNode, frame: timerFrame)
|
||||
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
|
||||
|
||||
let centralButtonSide = min(size.width, size.height) - 32.0
|
||||
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
|
||||
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
|
||||
let sideButtonMinimalInset: CGFloat = 16.0
|
||||
let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0))
|
||||
@@ -3037,48 +3198,76 @@ public final class VoiceChatController: ViewController {
|
||||
let actionButtonTitle: String
|
||||
let actionButtonSubtitle: String
|
||||
var actionButtonEnabled = true
|
||||
if let callState = self.callState {
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
if let callState = self.callState, !self.isScheduling {
|
||||
var isScheduled = callState.scheduleTimestamp != nil
|
||||
if isScheduled {
|
||||
self.ignoreNextConnecting = true
|
||||
if callState.canManageCall {
|
||||
actionButtonState = .scheduled(state: .start)
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow
|
||||
actionButtonSubtitle = ""
|
||||
} else {
|
||||
actionButtonState = .scheduled(state: .subscribe)
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder
|
||||
actionButtonSubtitle = ""
|
||||
}
|
||||
} else {
|
||||
let connected = self.ignoreNextConnecting || callState.networkState == .connected
|
||||
if case .connected = callState.networkState {
|
||||
self.ignoreNextConnecting = false
|
||||
}
|
||||
|
||||
if connected {
|
||||
if let muteState = callState.muteState, !self.pushingToTalk {
|
||||
if muteState.canUnmute {
|
||||
actionButtonState = .active(state: .muted)
|
||||
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
|
||||
actionButtonSubtitle = ""
|
||||
} else {
|
||||
actionButtonState = .active(state: .cantSpeak)
|
||||
|
||||
if callState.raisedHand {
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
|
||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
|
||||
} else {
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
|
||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionButtonState = .active(state: .on)
|
||||
|
||||
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
|
||||
actionButtonSubtitle = ""
|
||||
}
|
||||
} else {
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
actionButtonEnabled = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.isScheduling {
|
||||
actionButtonState = .button(text: self.scheduleButtonTitle)
|
||||
actionButtonTitle = ""
|
||||
actionButtonSubtitle = ""
|
||||
actionButtonEnabled = true
|
||||
} else {
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
actionButtonEnabled = false
|
||||
case .connected:
|
||||
if let muteState = callState.muteState, !self.pushingToTalk {
|
||||
if muteState.canUnmute {
|
||||
actionButtonState = .active(state: .muted)
|
||||
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
|
||||
actionButtonSubtitle = ""
|
||||
} else {
|
||||
actionButtonState = .active(state: .cantSpeak)
|
||||
|
||||
if callState.raisedHand {
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
|
||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
|
||||
} else {
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
|
||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionButtonState = .active(state: .on)
|
||||
|
||||
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
|
||||
actionButtonSubtitle = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
actionButtonEnabled = false
|
||||
}
|
||||
|
||||
self.actionButton.isDisabled = !actionButtonEnabled
|
||||
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
|
||||
|
||||
let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate)
|
||||
self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight)
|
||||
|
||||
if self.actionButton.supernode === self.bottomPanelNode {
|
||||
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame)
|
||||
}
|
||||
@@ -3196,6 +3385,12 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
|
||||
self.listNode.alpha = 0.0
|
||||
self.backgroundNode.backgroundColor = panelBackgroundColor
|
||||
self.updateIsFullscreen(false)
|
||||
}
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
let isFirstTime = self.isFirstTime
|
||||
if isFirstTime {
|
||||
@@ -3235,7 +3430,11 @@ public final class VoiceChatController: ViewController {
|
||||
let listTopInset = layoutTopInset + 63.0
|
||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
|
||||
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
|
||||
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
|
||||
} else {
|
||||
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
|
||||
}
|
||||
|
||||
let targetY = listTopInset + (self.topInset ?? listSize.height)
|
||||
|
||||
@@ -3453,9 +3652,12 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is DirectionalPanGestureRecognizer {
|
||||
if gestureRecognizer is UILongPressGestureRecognizer {
|
||||
return !self.isScheduling
|
||||
} else if gestureRecognizer is DirectionalPanGestureRecognizer {
|
||||
let location = gestureRecognizer.location(in: self.bottomPanelNode.view)
|
||||
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) {
|
||||
let containerLocation = gestureRecognizer.location(in: self.contentContainer.view)
|
||||
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) || self.pickerView?.frame.contains(containerLocation) == true {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -3494,6 +3696,9 @@ public final class VoiceChatController: ViewController {
|
||||
self.controller?.dismissAllTooltips()
|
||||
case .changed:
|
||||
var translation = recognizer.translation(in: self.contentContainer.view).y
|
||||
if (self.isScheduling || self.callState?.scheduleTimestamp != nil) && translation < 0.0 {
|
||||
return
|
||||
}
|
||||
var topInset: CGFloat = 0.0
|
||||
if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
|
||||
topInset = currentTopInset
|
||||
@@ -3591,9 +3796,13 @@ public final class VoiceChatController: ViewController {
|
||||
self.panGestureArguments = nil
|
||||
var dismissing = false
|
||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
|
||||
self.controller?.dismiss(closing: false, manual: true)
|
||||
if self.isScheduling {
|
||||
self.dismissScheduled()
|
||||
} else {
|
||||
self.controller?.dismiss(closing: false, manual: true)
|
||||
}
|
||||
dismissing = true
|
||||
} else if velocity.y < -300.0 || offset < topInset / 2.0 {
|
||||
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
|
||||
if velocity.y > -1500.0 && !self.isFullscreen {
|
||||
DispatchQueue.main.async {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
@@ -3610,7 +3819,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
|
||||
self.animatingExpansion = false
|
||||
})
|
||||
} else {
|
||||
} else if !self.isScheduling {
|
||||
self.updateIsFullscreen(false)
|
||||
self.animatingExpansion = true
|
||||
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||
@@ -3684,6 +3893,24 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func openTitleEditing() {
|
||||
let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title {
|
||||
strongSelf.call.updateTitle(title)
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
strongSelf.controller?.present(controller, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
guard let peerId = self.callState?.myPeerId else {
|
||||
return
|
||||
@@ -3765,7 +3992,7 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let proceed = {
|
||||
let proceed = {
|
||||
let _ = strongSelf.currentAvatarMixin.swap(nil)
|
||||
let postbox = strongSelf.context.account.postbox
|
||||
strongSelf.updateAvatarDisposable.set((updatePeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, accountPeerId: strongSelf.context.account.peerId, peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
|
||||
@@ -4096,6 +4323,8 @@ public final class VoiceChatController: ViewController {
|
||||
let count = navigationController.viewControllers.count
|
||||
if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
|
||||
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue {
|
||||
} else if case .button = self.controllerNode.actionButton.stateValue {
|
||||
} else if case .scheduled = self.controllerNode.actionButton.stateValue {
|
||||
} else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible {
|
||||
} else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
|
||||
} else {
|
||||
|
||||
@@ -145,7 +145,7 @@ public final class VoiceChatJoinScreen: ViewController {
|
||||
defaultJoinAsPeerId = cachedData.callJoinPeerId
|
||||
}
|
||||
|
||||
let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title)
|
||||
let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title, scheduleTimestamp: call.info.scheduleTimestamp, subscribed: false)
|
||||
if availablePeers.count > 0 && defaultJoinAsPeerId == nil {
|
||||
strongSelf.dismiss()
|
||||
strongSelf.join(activeCall)
|
||||
|
||||
@@ -396,7 +396,7 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
var slide = true
|
||||
var hidden = true
|
||||
var animated = true
|
||||
var animateInsets = true
|
||||
|
||||
if controllers.count == 1 || controllers.last is ChatController {
|
||||
if let chatController = controllers.last as? ChatController {
|
||||
slide = false
|
||||
@@ -416,9 +416,13 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
hidden = true
|
||||
}
|
||||
|
||||
if case .active(.cantSpeak) = state {
|
||||
hidden = true
|
||||
switch state {
|
||||
case .active(.cantSpeak), .button, .scheduled:
|
||||
hidden = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if hasVoiceChatController {
|
||||
hidden = false
|
||||
animated = self.initiallyHidden
|
||||
@@ -429,7 +433,6 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
|
||||
let previousInsets = self.additionalSideInsets
|
||||
self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
|
||||
|
||||
if previousInsets != self.additionalSideInsets {
|
||||
self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
private let pink = UIColor(rgb: 0xef436c)
|
||||
|
||||
final class VoiceChatTimerNode: ASDisplayNode {
|
||||
private let strings: PresentationStrings
|
||||
private let dateTimeFormat: PresentationDateTimeFormat
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let subtitleNode: ImmediateTextNode
|
||||
|
||||
private let timerNode: ImmediateTextNode
|
||||
|
||||
private let foregroundView = UIView()
|
||||
private let foregroundGradientLayer = CAGradientLayer()
|
||||
private let maskView = UIView()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var updateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) {
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.subtitleNode = ImmediateTextNode()
|
||||
|
||||
self.timerNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.allowsGroupOpacity = true
|
||||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.foregroundView.mask = self.maskView
|
||||
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
|
||||
|
||||
self.view.addSubview(self.foregroundView)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
|
||||
self.maskView.addSubnode(self.timerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.updateTimer?.invalidate()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.foregroundView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||
} else {
|
||||
let previousValue = self.foregroundGradientLayer.startPoint
|
||||
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45))
|
||||
self.foregroundGradientLayer.startPoint = newValue
|
||||
|
||||
CATransaction.begin()
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "startPoint")
|
||||
animation.duration = Double.random(in: 0.8 ..< 1.4)
|
||||
animation.fromValue = previousValue
|
||||
animation.toValue = newValue
|
||||
|
||||
CATransaction.setCompletionBlock { [weak self] in
|
||||
// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
|
||||
self?.setupGradientAnimations()
|
||||
// }
|
||||
}
|
||||
|
||||
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, scheduleTime: Int32?, transition: ContainedViewLayoutTransition) {
|
||||
if self.validLayout == nil {
|
||||
self.setupGradientAnimations()
|
||||
}
|
||||
self.validLayout = size
|
||||
|
||||
guard let scheduleTime = scheduleTime else {
|
||||
return
|
||||
}
|
||||
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.foregroundGradientLayer.frame = self.foregroundView.bounds
|
||||
self.maskView.frame = self.foregroundView.bounds
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let elapsedTime = scheduleTime - currentTime
|
||||
let timerText: String
|
||||
if elapsedTime >= 86400 {
|
||||
timerText = timeIntervalString(strings: self.strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
timerText = "\(textForTimeout(value: abs(elapsedTime)))"
|
||||
} else {
|
||||
timerText = textForTimeout(value: elapsedTime)
|
||||
}
|
||||
|
||||
if self.updateTimer == nil {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self, let size = strongSelf.validLayout {
|
||||
strongSelf.update(size: size, scheduleTime: scheduleTime, transition: .immediate)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.updateTimer = timer
|
||||
timer.start()
|
||||
}
|
||||
|
||||
let subtitle = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime)
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: elapsedTime < 0 ? self.strings.VoiceChat_LateBy : self.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
let titleSize = self.titleNode.updateLayout(size)
|
||||
self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
|
||||
|
||||
self.timerNode.attributedText = NSAttributedString(string: timerText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
|
||||
let timerSize = self.timerNode.updateLayout(size)
|
||||
self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 80.0, width: timerSize.width, height: timerSize.height)
|
||||
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 21.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
let subtitleSize = self.subtitleNode.updateLayout(size)
|
||||
self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: timerSize.width, height: subtitleSize.height)
|
||||
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
|
||||
private let maxLength: Int
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String, maxLength: Int) {
|
||||
init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
|
||||
self.theme = theme
|
||||
self.maxLength = maxLength
|
||||
|
||||
@@ -65,7 +65,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .default
|
||||
self.textInputNode.autocapitalizationType = .sentences
|
||||
self.textInputNode.returnKeyType = .done
|
||||
self.textInputNode.returnKeyType = returnKeyType
|
||||
self.textInputNode.autocorrectionType = .default
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
@@ -510,7 +510,7 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength)
|
||||
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next)
|
||||
self.firstNameInputFieldNode.text = firstNameValue ?? ""
|
||||
|
||||
self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
|
||||
@@ -550,14 +550,6 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.firstNameInputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -271,9 +271,9 @@ public func startScheduledGroupCall(account: Account, peerId: PeerId, callId: In
|
||||
return account.postbox.transaction { transaction -> GroupCallInfo in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false))
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribed: false))
|
||||
return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribed: false))
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
@@ -343,15 +343,27 @@ public func updateGroupCallJoinAsPeer(account: Account, peerId: PeerId, joinAs:
|
||||
}
|
||||
|> castError(UpdateGroupCallJoinAsPeerError.self)
|
||||
|> mapToSignal { result in
|
||||
guard let (peer, joinAs) = result else {
|
||||
guard let (inputPeer, joinInputPeer) = result else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: peer, joinAs: joinAs))
|
||||
return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: inputPeer, joinAs: joinInputPeer))
|
||||
|> mapError { _ -> UpdateGroupCallJoinAsPeerError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, UpdateGroupCallJoinAsPeerError> in
|
||||
return .complete()
|
||||
return account.postbox.transaction { transaction in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
})
|
||||
}
|
||||
|> castError(UpdateGroupCallJoinAsPeerError.self)
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,9 +668,9 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
||||
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||
return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribed: false))
|
||||
} else {
|
||||
return cachedData
|
||||
}
|
||||
@@ -1144,11 +1156,24 @@ public final class GroupCallParticipantsContext {
|
||||
|
||||
public var state: Signal<State, NoError> {
|
||||
let accountPeerId = self.account.peerId
|
||||
let myPeerId = self.myPeerId
|
||||
return self.statePromise.get()
|
||||
|> map { state -> State in
|
||||
var publicState = state.state
|
||||
var sortAgain = false
|
||||
let canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId)
|
||||
var canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId)
|
||||
for participant in publicState.participants {
|
||||
if participant.peer.id == myPeerId {
|
||||
if let muteState = participant.muteState {
|
||||
if muteState.canUnmute {
|
||||
canSeeHands = true
|
||||
}
|
||||
} else {
|
||||
canSeeHands = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for i in 0 ..< publicState.participants.count {
|
||||
if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] {
|
||||
publicState.participants[i].muteState = pendingMuteState.state
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ public func setupCurrencyNumberFormatter(currency: String) -> NumberFormatter {
|
||||
numberFormatter.positiveFormat = result
|
||||
numberFormatter.negativeFormat = "-\(result)"
|
||||
|
||||
numberFormatter.currencySymbol = entry.symbol
|
||||
numberFormatter.currencySymbol = ""
|
||||
numberFormatter.currencyDecimalSeparator = entry.decimalSeparator
|
||||
numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator
|
||||
|
||||
@@ -164,3 +164,42 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String {
|
||||
return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String) {
|
||||
if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
|
||||
var result = ""
|
||||
if amount < 0 {
|
||||
result.append("-")
|
||||
}
|
||||
/*if entry.symbolOnLeft {
|
||||
result.append(entry.symbol)
|
||||
if entry.spaceBetweenAmountAndSymbol {
|
||||
result.append(" ")
|
||||
}
|
||||
}*/
|
||||
var integerPart = abs(amount)
|
||||
var fractional: [Character] = []
|
||||
for _ in 0 ..< entry.decimalDigits {
|
||||
let part = integerPart % 10
|
||||
integerPart /= 10
|
||||
if let scalar = UnicodeScalar(UInt32(part + 48)) {
|
||||
fractional.append(Character(scalar))
|
||||
}
|
||||
}
|
||||
result.append("\(integerPart)")
|
||||
result.append(entry.decimalSeparator)
|
||||
for i in 0 ..< fractional.count {
|
||||
result.append(fractional[fractional.count - i - 1])
|
||||
}
|
||||
/*if !entry.symbolOnLeft {
|
||||
if entry.spaceBetweenAmountAndSymbol {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(entry.symbol)
|
||||
}*/
|
||||
|
||||
return (result, entry.symbol)
|
||||
} else {
|
||||
return ("", "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public enum MessageContentKind: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind {
|
||||
public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> MessageContentKind {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) {
|
||||
@@ -95,14 +95,14 @@ public func messageContentKind(contentSettings: ContentSettings, message: Messag
|
||||
}
|
||||
}
|
||||
for media in message.media {
|
||||
if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) {
|
||||
if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) {
|
||||
return kind
|
||||
}
|
||||
}
|
||||
return .text(message.text)
|
||||
}
|
||||
|
||||
public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? {
|
||||
public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? {
|
||||
switch media {
|
||||
case let expiredMedia as TelegramMediaExpiredContent:
|
||||
switch expiredMedia.data {
|
||||
@@ -163,7 +163,7 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P
|
||||
}
|
||||
case _ as TelegramMediaAction:
|
||||
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
|
||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@@ -223,8 +223,8 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) {
|
||||
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId)
|
||||
public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> (String, Bool) {
|
||||
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
||||
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
|
||||
return (foldLineBreaks(message.text), false)
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P
|
||||
return result
|
||||
}
|
||||
|
||||
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
|
||||
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
|
||||
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? {
|
||||
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
|
||||
}
|
||||
|
||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? {
|
||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? {
|
||||
var attributedString: NSAttributedString?
|
||||
|
||||
let primaryTextColor: UIColor
|
||||
@@ -448,7 +448,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||
case let .groupPhoneCall(_, _, scheduleDate, duration):
|
||||
if let scheduleDate = scheduleDate {
|
||||
let titleString = strings.Notification_VoiceChatScheduled
|
||||
let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
|
||||
let titleString = strings.Notification_VoiceChatScheduled(timeString).0
|
||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||
} else if let duration = duration {
|
||||
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "callshare (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -302,6 +302,10 @@ public final class AccountContextImpl: AccountContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func scheduleGroupCall(peerId: PeerId) {
|
||||
let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true)
|
||||
}
|
||||
|
||||
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) {
|
||||
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
|
||||
@@ -356,7 +356,7 @@ final class AuthorizedApplicationContext {
|
||||
|
||||
if inAppNotificationSettings.displayPreviews {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: {
|
||||
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: {
|
||||
if let strongSelf = self {
|
||||
var foundOverlay = false
|
||||
strongSelf.mainWindow.forEachViewController({ controller in
|
||||
|
||||
@@ -177,14 +177,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
||||
controller.inProgress = false
|
||||
|
||||
let text: String
|
||||
var actions: [TextAlertAction] = [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
||||
]
|
||||
var actions: [TextAlertAction] = []
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = strongSelf.presentationData.strings.Login_CodeFloodError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
case .invalidPhoneNumber:
|
||||
text = strongSelf.presentationData.strings.Login_InvalidPhoneError
|
||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||
guard let strongSelf = self, let controller = controller else {
|
||||
return
|
||||
@@ -200,8 +200,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
||||
}))
|
||||
case .phoneLimitExceeded:
|
||||
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
case .phoneBanned:
|
||||
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||
guard let strongSelf = self, let controller = controller else {
|
||||
return
|
||||
@@ -217,6 +219,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
||||
}))
|
||||
case let .generic(info):
|
||||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
|
||||
guard let strongSelf = self, let controller = controller else {
|
||||
return
|
||||
@@ -238,6 +241,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
||||
}))
|
||||
case .timeout:
|
||||
text = strongSelf.presentationData.strings.Login_NetworkError
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in
|
||||
guard let strongSelf = self, let controller = controller else {
|
||||
return
|
||||
|
||||
@@ -535,7 +535,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
||||
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title))
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribed: activeCall.subscribed))
|
||||
} else {
|
||||
var canManageGroupCalls = false
|
||||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
|
||||
@@ -564,12 +564,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
statusController?.dismiss()
|
||||
}
|
||||
strongSelf.present(statusController, in: .window(.root))
|
||||
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId)
|
||||
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId, title: nil, scheduleDate: nil)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribed: false))
|
||||
}, error: { [weak self] error in
|
||||
dismissStatus?()
|
||||
|
||||
@@ -912,6 +912,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}
|
||||
}, activateMessagePinch: { [weak self] sourceNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let pinchController = PinchController(sourceNode: sourceNode)
|
||||
strongSelf.window?.presentInGlobalOverlay(pinchController)
|
||||
}, openMessageContextActions: { message, node, rect, gesture in
|
||||
gesture?.cancel()
|
||||
}, navigateToMessage: { [weak self] fromId, id in
|
||||
|
||||
@@ -53,6 +53,7 @@ public final class ChatControllerInteraction {
|
||||
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void
|
||||
let openPeerMention: (String) -> Void
|
||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
|
||||
let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
||||
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
let navigateToMessage: (MessageId, MessageId) -> Void
|
||||
let navigateToMessageStandalone: (MessageId) -> Void
|
||||
@@ -144,6 +145,7 @@ public final class ChatControllerInteraction {
|
||||
openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void,
|
||||
openPeerMention: @escaping (String) -> Void,
|
||||
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void,
|
||||
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
|
||||
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
||||
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
|
||||
navigateToMessageStandalone: @escaping (MessageId) -> Void,
|
||||
@@ -222,6 +224,7 @@ public final class ChatControllerInteraction {
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
self.openMessageContextMenu = openMessageContextMenu
|
||||
self.activateMessagePinch = activateMessagePinch
|
||||
self.openMessageContextActions = openMessageContextActions
|
||||
self.navigateToMessage = navigateToMessage
|
||||
self.navigateToMessageStandalone = navigateToMessageStandalone
|
||||
@@ -301,7 +304,7 @@ public final class ChatControllerInteraction {
|
||||
|
||||
static var `default`: ChatControllerInteraction {
|
||||
return ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
|
||||
@@ -32,7 +32,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
||||
editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
return editPanelNode
|
||||
} else {
|
||||
let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder)
|
||||
let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
|
||||
panelNode.interfaceInteraction = interfaceInteraction
|
||||
return panelNode
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
||||
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
return replyPanelNode
|
||||
} else {
|
||||
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder)
|
||||
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
|
||||
panelNode.interfaceInteraction = interfaceInteraction
|
||||
return panelNode
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
import GalleryUI
|
||||
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false)
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false)
|
||||
}
|
||||
|
||||
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
@@ -132,7 +132,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, accountPeerId: item.context.account.peerId)
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId)
|
||||
|
||||
var image: TelegramMediaImage?
|
||||
for media in item.message.media {
|
||||
|
||||
@@ -143,6 +143,7 @@ class ChatMessageShareButton: HighlightableButtonNode {
|
||||
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
let imageNode: TransformImageNode
|
||||
private var placeholderNode: StickerShimmerEffectNode
|
||||
private var animationNode: GenericAnimatedStickerNode?
|
||||
@@ -195,6 +196,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
@@ -262,7 +264,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
@@ -278,6 +281,23 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
item.controllerInteraction.openMessageReactions(item.message.id)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.activate = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -976,6 +996,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
||||
@@ -1063,6 +1085,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
imageApply()
|
||||
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
if let updatedShareButtonNode = updatedShareButtonNode {
|
||||
|
||||
@@ -271,7 +271,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let textAsyncLayout = TextNode.asyncLayout(self.textNode)
|
||||
let currentImage = self.media as? TelegramMediaImage
|
||||
let imageLayout = self.inlineImageNode.asyncLayout()
|
||||
@@ -284,7 +284,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
|
||||
let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode
|
||||
|
||||
return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in
|
||||
return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize in
|
||||
let isPreview = presentationData.isPreview
|
||||
let fontSize: CGFloat = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0)
|
||||
|
||||
@@ -420,11 +420,99 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
if case .replyThread = chatLocation {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
var imageMode = false
|
||||
|
||||
var textStatusType: ChatMessageDateAndStatusType?
|
||||
var imageStatusType: ChatMessageDateAndStatusType?
|
||||
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
|
||||
|
||||
if let (media, flags) = mediaAndFlags {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
||||
imageMode = true
|
||||
} else if file.isInstantVideo {
|
||||
imageMode = true
|
||||
} else if file.isVideo {
|
||||
imageMode = true
|
||||
} else if file.isSticker || file.isAnimatedSticker {
|
||||
imageMode = true
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaImage {
|
||||
if !flags.contains(.preferMediaInline) {
|
||||
imageMode = true
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaWebFile {
|
||||
imageMode = true
|
||||
} else if let _ = media as? WallpaperPreviewMedia {
|
||||
imageMode = true
|
||||
}
|
||||
}
|
||||
|
||||
if preferMediaBeforeText {
|
||||
imageMode = false
|
||||
}
|
||||
|
||||
let statusInText = !imageMode
|
||||
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if let count = webpageGalleryMediaCount {
|
||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0))
|
||||
skipStandardStatus = imageMode
|
||||
} else if let mediaBadge = mediaBadge {
|
||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge))
|
||||
}
|
||||
|
||||
if !skipStandardStatus {
|
||||
if message.effectivelyIncoming(context.account.peerId) {
|
||||
if imageMode {
|
||||
imageStatusType = .ImageIncoming
|
||||
} else {
|
||||
textStatusType = .BubbleIncoming
|
||||
}
|
||||
} else {
|
||||
if message.flags.contains(.Failed) {
|
||||
if imageMode {
|
||||
imageStatusType = .ImageOutgoing(.Failed)
|
||||
} else {
|
||||
textStatusType = .BubbleOutgoing(.Failed)
|
||||
}
|
||||
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
||||
if imageMode {
|
||||
imageStatusType = .ImageOutgoing(.Sending)
|
||||
} else {
|
||||
textStatusType = .BubbleOutgoing(.Sending)
|
||||
}
|
||||
} else {
|
||||
if imageMode {
|
||||
imageStatusType = .ImageOutgoing(.Sent(read: messageRead))
|
||||
} else {
|
||||
textStatusType = .BubbleOutgoing(.Sent(read: messageRead))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in
|
||||
ChatMessageDateAndStatus(
|
||||
type: statusType,
|
||||
edited: edited,
|
||||
viewCount: viewCount,
|
||||
dateReplies: dateReplies,
|
||||
dateReactions: dateReactions,
|
||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
||||
dateText: dateText
|
||||
)
|
||||
}
|
||||
|
||||
if let (media, flags) = mediaAndFlags {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else if file.isInstantVideo {
|
||||
@@ -455,12 +543,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else if file.isSticker || file.isAnimatedSticker {
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else {
|
||||
@@ -485,7 +573,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if !flags.contains(.preferMediaInline) {
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
@@ -497,11 +585,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
} else if let image = media as? TelegramMediaWebFile {
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else if let wallpaper = media as? WallpaperPreviewMedia {
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme {
|
||||
@@ -527,60 +615,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
break
|
||||
}
|
||||
|
||||
var statusInText = false
|
||||
|
||||
var statusSizeAndApply: (CGSize, (Bool) -> Void)?
|
||||
|
||||
|
||||
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
|
||||
|
||||
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
|
||||
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText)
|
||||
statusInText = !imageMode
|
||||
|
||||
if let count = webpageGalleryMediaCount {
|
||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0))
|
||||
skipStandardStatus = imageMode
|
||||
} else if let mediaBadge = mediaBadge {
|
||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge))
|
||||
}
|
||||
|
||||
if !skipStandardStatus {
|
||||
let statusType: ChatMessageDateAndStatusType
|
||||
if message.effectivelyIncoming(context.account.peerId) {
|
||||
if imageMode {
|
||||
statusType = .ImageIncoming
|
||||
} else {
|
||||
statusType = .BubbleIncoming
|
||||
}
|
||||
} else {
|
||||
if message.flags.contains(.Failed) {
|
||||
if imageMode {
|
||||
statusType = .ImageOutgoing(.Failed)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
}
|
||||
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
||||
if imageMode {
|
||||
statusType = .ImageOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
}
|
||||
} else {
|
||||
if imageMode {
|
||||
statusType = .ImageOutgoing(.Sent(read: messageRead))
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: messageRead))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
if let textStatusType = textStatusType {
|
||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
||||
}
|
||||
|
||||
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
|
||||
@@ -823,6 +863,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
let contentImageNode = contentImageApply(transition, synchronousLoads)
|
||||
if strongSelf.contentImageNode !== contentImageNode {
|
||||
strongSelf.contentImageNode = contentImageNode
|
||||
contentImageNode.activatePinch = { sourceNode in
|
||||
controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
strongSelf.addSubnode(contentImageNode)
|
||||
contentImageNode.activateLocalContent = { [weak strongSelf] mode in
|
||||
if let strongSelf = strongSelf {
|
||||
|
||||
@@ -236,7 +236,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let clockMinImage: UIImage?
|
||||
var impressionImage: UIImage?
|
||||
var repliesImage: UIImage?
|
||||
var selfExpiringImage: UIImage?
|
||||
let selfExpiringImage: UIImage? = nil
|
||||
|
||||
let themeUpdated = presentationData.theme != currentTheme || type != currentType
|
||||
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
@@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble
|
||||
}
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
@@ -39,7 +39,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent
|
||||
let text: String = item.message.text
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
@@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont
|
||||
}
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var game: TelegramMediaGame?
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
|
||||
@@ -78,7 +78,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import TelegramAnimatedStickerNode
|
||||
import LocalMediaResources
|
||||
import WallpaperResources
|
||||
import ChatMessageInteractiveMediaBadge
|
||||
import ContextUI
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: (Bool) -> Void
|
||||
@@ -64,9 +65,23 @@ enum InteractiveMediaNodePlayWithSoundMode {
|
||||
case loop
|
||||
}
|
||||
|
||||
struct ChatMessageDateAndStatus {
|
||||
var type: ChatMessageDateAndStatusType
|
||||
var edited: Bool
|
||||
var viewCount: Int?
|
||||
var dateReplies: Int
|
||||
var dateReactions: [MessageReaction]
|
||||
var isPinned: Bool
|
||||
var dateText: String
|
||||
}
|
||||
|
||||
final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode {
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var currentImageArguments: TransformImageArguments?
|
||||
private var currentHighQualityImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)?
|
||||
private var highQualityImageNode: TransformImageNode?
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var videoContent: NativeVideoContent?
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
@@ -75,6 +90,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
var decoration: UniversalVideoDecoration? {
|
||||
return self.videoNodeDecoration
|
||||
}
|
||||
let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var badgeNode: ChatMessageInteractiveMediaBadge?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
@@ -134,15 +150,103 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
|
||||
var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in }
|
||||
var activatePinch: ((PinchSourceContainerNode) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.addSubnode(self.imageNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
||||
|
||||
self.pinchContainerNode.activate = { [weak self] sourceNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activatePinch?(sourceNode)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
|
||||
|
||||
if abs(scale - 1.0) > CGFloat.ulpOfOne {
|
||||
var highQualityImageNode: TransformImageNode?
|
||||
if let current = strongSelf.highQualityImageNode {
|
||||
highQualityImageNode = current
|
||||
} else if let (currentHighQualityImageSignal, nativeImageSize) = strongSelf.currentHighQualityImageSignal, let currentImageArguments = strongSelf.currentImageArguments {
|
||||
let imageNode = TransformImageNode()
|
||||
imageNode.frame = strongSelf.imageNode.frame
|
||||
|
||||
let corners = currentImageArguments.corners
|
||||
if isRoundEqualCorners(corners) {
|
||||
imageNode.cornerRadius = corners.topLeft.radius
|
||||
imageNode.layer.mask = nil
|
||||
} else {
|
||||
imageNode.cornerRadius = 0
|
||||
|
||||
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
|
||||
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
|
||||
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
let context = DrawingContext(size: size, clear: true)
|
||||
context.withContext { ctx in
|
||||
ctx.setFillColor(UIColor.black.cgColor)
|
||||
ctx.fill(arguments.drawingRect)
|
||||
}
|
||||
addCorners(context, arguments: arguments)
|
||||
|
||||
if let maskImage = context.generateImage() {
|
||||
let mask = CALayer()
|
||||
mask.contents = maskImage.cgImage
|
||||
mask.contentsScale = maskImage.scale
|
||||
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
|
||||
|
||||
imageNode.layer.mask = mask
|
||||
imageNode.layer.mask?.frame = imageNode.bounds
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.pinchContainerNode.contentNode.insertSubnode(imageNode, aboveSubnode: strongSelf.imageNode)
|
||||
|
||||
let scaleFactor = nativeImageSize.height / currentImageArguments.imageSize.height
|
||||
|
||||
let apply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: currentImageArguments.imageSize.width * scaleFactor, height: currentImageArguments.imageSize.height * scaleFactor), boundingSize: CGSize(width: currentImageArguments.boundingSize.width * scaleFactor, height: currentImageArguments.boundingSize.height * scaleFactor), intrinsicInsets: UIEdgeInsets(top: currentImageArguments.intrinsicInsets.top * scaleFactor, left: currentImageArguments.intrinsicInsets.left * scaleFactor, bottom: currentImageArguments.intrinsicInsets.bottom * scaleFactor, right: currentImageArguments.intrinsicInsets.right * scaleFactor)))
|
||||
let _ = apply()
|
||||
imageNode.setSignal(currentHighQualityImageSignal, attemptSynchronously: false)
|
||||
|
||||
highQualityImageNode = imageNode
|
||||
strongSelf.highQualityImageNode = imageNode
|
||||
}
|
||||
if let highQualityImageNode = highQualityImageNode {
|
||||
transition.updateAlpha(node: highQualityImageNode, alpha: factor)
|
||||
}
|
||||
} else if let highQualityImageNode = strongSelf.highQualityImageNode {
|
||||
strongSelf.highQualityImageNode = nil
|
||||
transition.updateAlpha(node: highQualityImageNode, alpha: 0.0, completion: { [weak highQualityImageNode] _ in
|
||||
highQualityImageNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let badgeNode = strongSelf.badgeNode {
|
||||
transition.updateAlpha(node: badgeNode, alpha: 1.0 - factor)
|
||||
}
|
||||
if let statusNode = strongSelf.statusNode {
|
||||
transition.updateAlpha(node: statusNode, alpha: 1.0 - factor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -242,10 +346,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) {
|
||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) {
|
||||
let currentMessage = self.message
|
||||
let currentMedia = self.media
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
let statusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
|
||||
let currentVideoNode = self.videoNode
|
||||
let currentAnimatedStickerNode = self.animatedStickerNode
|
||||
@@ -255,7 +360,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
let currentAutomaticDownload = self.automaticDownload
|
||||
let currentAutomaticPlayback = self.automaticPlayback
|
||||
|
||||
return { [weak self] context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
|
||||
return { [weak self] context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
|
||||
var nativeSize: CGSize
|
||||
|
||||
let isSecretMedia = message.containsSecretMedia
|
||||
@@ -359,6 +464,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
case .unconstrained:
|
||||
nativeSize = unboundSize
|
||||
}
|
||||
|
||||
var statusSize = CGSize()
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let dateAndStatus = dateAndStatus {
|
||||
let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReactions, dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
|
||||
let maxWidth: CGFloat
|
||||
if isSecretMedia {
|
||||
@@ -367,7 +481,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
maxWidth = maxDimensions.width
|
||||
}
|
||||
if isSecretMedia {
|
||||
let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme)
|
||||
let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme)
|
||||
}
|
||||
|
||||
return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in
|
||||
@@ -416,7 +530,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
drawingSize = nativeSize.aspectFilled(boundingSize)
|
||||
}
|
||||
|
||||
var updateImageSignal: ((Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)?
|
||||
var updateImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)?
|
||||
var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>?
|
||||
var updatedFetchControls: FetchControls?
|
||||
|
||||
@@ -453,7 +567,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if isSticker {
|
||||
emptyColor = .clear
|
||||
} else {
|
||||
emptyColor = message.effectivelyIncoming(context.account.peerId) ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
emptyColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
}
|
||||
if let wallpaper = media as? WallpaperPreviewMedia {
|
||||
if case let .file(_, patternColor, patternBottomColor, rotation, _, _) = wallpaper.content {
|
||||
@@ -475,12 +589,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
replaceAnimatedStickerNode = true
|
||||
}
|
||||
if isSecretMedia {
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image))
|
||||
}
|
||||
} else {
|
||||
updateImageSignal = { synchronousLoad in
|
||||
return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad)
|
||||
updateImageSignal = { synchronousLoad, highQuality in
|
||||
return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +619,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if hasCurrentAnimatedStickerNode {
|
||||
replaceAnimatedStickerNode = true
|
||||
}
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return chatWebFileImage(account: context.account, file: image)
|
||||
}
|
||||
|
||||
@@ -518,22 +632,22 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
})
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if isSecretMedia {
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return chatSecretMessageVideo(account: context.account, videoReference: .message(message: MessageReference(message), media: file))
|
||||
}
|
||||
} else {
|
||||
if file.isAnimatedSticker {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)))
|
||||
}
|
||||
} else if file.isSticker {
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return chatMessageSticker(account: context.account, file: file, small: false)
|
||||
}
|
||||
} else {
|
||||
onlyFullSizeVideoThumbnail = isSendingUpdated
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
return mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true)
|
||||
}
|
||||
}
|
||||
@@ -598,7 +712,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
})
|
||||
} else if let wallpaper = media as? WallpaperPreviewMedia {
|
||||
updateImageSignal = { synchronousLoad in
|
||||
updateImageSignal = { synchronousLoad, _ in
|
||||
switch wallpaper.content {
|
||||
case let .file(file, _, _, _, isTheme, _):
|
||||
if isTheme {
|
||||
@@ -692,27 +806,52 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
strongSelf.attributes = attributes
|
||||
strongSelf.media = media
|
||||
strongSelf.wideLayout = wideLayout
|
||||
strongSelf.themeAndStrings = (theme, strings, dateTimeFormat.decimalSeparator)
|
||||
strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator)
|
||||
strongSelf.sizeCalculation = sizeCalculation
|
||||
strongSelf.automaticPlayback = automaticPlayback
|
||||
strongSelf.automaticDownload = automaticDownload
|
||||
|
||||
if let previousArguments = strongSelf.currentImageArguments {
|
||||
if previousArguments.imageSize == arguments.imageSize {
|
||||
strongSelf.imageNode.frame = imageFrame
|
||||
strongSelf.pinchContainerNode.frame = imageFrame
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate)
|
||||
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
||||
} else {
|
||||
transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame)
|
||||
transition.updateFrame(node: strongSelf.pinchContainerNode, frame: imageFrame)
|
||||
transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(), size: imageFrame.size))
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition)
|
||||
|
||||
}
|
||||
} else {
|
||||
strongSelf.imageNode.frame = imageFrame
|
||||
strongSelf.pinchContainerNode.frame = imageFrame
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate)
|
||||
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
||||
}
|
||||
strongSelf.currentImageArguments = arguments
|
||||
imageApply()
|
||||
|
||||
if let statusApply = statusApply {
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
var hasAnimation = true
|
||||
if transition.isAnimated {
|
||||
hasAnimation = false
|
||||
}
|
||||
statusApply(hasAnimation)
|
||||
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: imageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: imageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
|
||||
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
||||
strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let statusNode = strongSelf.statusNode {
|
||||
var statusFrame = statusNode.frame
|
||||
statusFrame.origin.x = floor(imageFrame.midX - statusFrame.width / 2.0)
|
||||
statusFrame.origin.y = floor(imageFrame.midY - statusFrame.height / 2.0)
|
||||
statusFrame.origin.x = floor(imageFrame.width / 2.0 - statusFrame.width / 2.0)
|
||||
statusFrame.origin.y = floor(imageFrame.height / 2.0 - statusFrame.height / 2.0)
|
||||
statusNode.frame = statusFrame
|
||||
}
|
||||
|
||||
@@ -776,7 +915,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
let dimensions = updatedAnimatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
strongSelf.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode)
|
||||
strongSelf.pinchContainerNode.contentNode.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode)
|
||||
animatedStickerNode.visibility = strongSelf.visibility
|
||||
}
|
||||
}
|
||||
@@ -807,7 +946,20 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
|
||||
if let updateImageSignal = updateImageSignal {
|
||||
strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads), attemptSynchronously: synchronousLoads)
|
||||
strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads)
|
||||
|
||||
var imageDimensions: CGSize?
|
||||
if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
} else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
} else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
|
||||
if let imageDimensions = imageDimensions {
|
||||
strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions)
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = secretBeginTimeAndTimeout {
|
||||
@@ -837,7 +989,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
|> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
||||
displayLinkDispatcher.dispatch {
|
||||
if let strongSelf = strongSelf, let videoNode = strongSelf.videoNode {
|
||||
strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
|
||||
strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -997,10 +1149,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if progressRequired {
|
||||
if self.statusNode == nil {
|
||||
let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor)
|
||||
let imagePosition = self.imageNode.position
|
||||
statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize))
|
||||
let imageSize = self.imageNode.bounds.size
|
||||
statusNode.frame = CGRect(origin: CGPoint(x: floor(imageSize.width / 2.0 - radialStatusSize / 2.0), y: floor(imageSize.height / 2.0 - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize))
|
||||
self.statusNode = statusNode
|
||||
self.addSubnode(statusNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(statusNode)
|
||||
}
|
||||
} else {
|
||||
if let statusNode = self.statusNode {
|
||||
@@ -1275,7 +1427,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
}
|
||||
self.badgeNode = badgeNode
|
||||
self.addSubnode(badgeNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(badgeNode)
|
||||
|
||||
animated = false
|
||||
}
|
||||
@@ -1300,12 +1452,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) {
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
return { context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
|
||||
return { context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
|
||||
var imageNode: ChatMessageInteractiveMediaNode
|
||||
var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void)))
|
||||
var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void)))
|
||||
|
||||
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||
imageNode = node
|
||||
@@ -1315,7 +1467,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
imageLayout = imageNode.asyncLayout()
|
||||
}
|
||||
|
||||
let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode)
|
||||
let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode)
|
||||
|
||||
return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in
|
||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners)
|
||||
|
||||
@@ -38,7 +38,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var invoice: TelegramMediaInvoice?
|
||||
for media in item.message.media {
|
||||
if let media = media as? TelegramMediaInvoice {
|
||||
@@ -74,7 +74,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData {
|
||||
if let chatPeer = message.peers[item.message.id.peerId] {
|
||||
let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
|
||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
||||
|
||||
var text = messageText
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
private let interactiveImageNode: ChatMessageInteractiveMediaNode
|
||||
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var selectionNode: GridMessageSelectionNode?
|
||||
private var highlightedState: Bool = false
|
||||
|
||||
@@ -32,7 +31,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
required init() {
|
||||
self.interactiveImageNode = ChatMessageInteractiveMediaNode()
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -54,6 +52,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let _ = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
strongSelf.item?.controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -62,7 +67,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let interactiveImageLayout = self.interactiveImageNode.asyncLayout()
|
||||
let statusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, preparePosition, selection, constrainedSize in
|
||||
var selectedMedia: Media?
|
||||
@@ -142,8 +146,81 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
bubbleInsets = UIEdgeInsets()
|
||||
sizeCalculation = .unconstrained
|
||||
}
|
||||
|
||||
var edited = false
|
||||
if item.attributes.updatingMedia != nil {
|
||||
edited = true
|
||||
}
|
||||
var viewCount: Int?
|
||||
var dateReplies = 0
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
if case .mosaic = preparePosition {
|
||||
} else {
|
||||
edited = !attribute.isHidden
|
||||
}
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
|
||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
||||
dateReplies = Int(attribute.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dateReactions: [MessageReaction] = []
|
||||
var dateReactionCount = 0
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty {
|
||||
for reaction in reactionsAttribute.reactions {
|
||||
if reaction.isSelected {
|
||||
dateReactions.insert(reaction, at: 0)
|
||||
} else {
|
||||
dateReactions.append(reaction)
|
||||
}
|
||||
dateReactionCount += Int(reaction.count)
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
statusType = .ImageIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .ImageOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .ImageOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .ImageOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
case .mosaic:
|
||||
statusType = nil
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
let dateAndStatus = statusType.flatMap { statusType -> ChatMessageDateAndStatus in
|
||||
ChatMessageDateAndStatus(
|
||||
type: statusType,
|
||||
edited: edited,
|
||||
viewCount: viewCount,
|
||||
dateReplies: dateReplies,
|
||||
dateReactions: dateReactions,
|
||||
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
||||
dateText: dateText
|
||||
)
|
||||
}
|
||||
|
||||
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode)
|
||||
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode)
|
||||
|
||||
let forceFullCorners = false
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none)
|
||||
@@ -169,82 +246,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in
|
||||
let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right)
|
||||
|
||||
var edited = false
|
||||
if item.attributes.updatingMedia != nil {
|
||||
edited = true
|
||||
}
|
||||
var viewCount: Int?
|
||||
var dateReplies = 0
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
if case .mosaic = preparePosition {
|
||||
} else {
|
||||
edited = !attribute.isHidden
|
||||
}
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
|
||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
||||
dateReplies = Int(attribute.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dateReactions: [MessageReaction] = []
|
||||
var dateReactionCount = 0
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty {
|
||||
for reaction in reactionsAttribute.reactions {
|
||||
if reaction.isSelected {
|
||||
dateReactions.insert(reaction, at: 0)
|
||||
} else {
|
||||
dateReactions.append(reaction)
|
||||
}
|
||||
dateReactionCount += Int(reaction.count)
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
statusType = .ImageIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .ImageOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .ImageOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .ImageOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
case .mosaic:
|
||||
statusType = nil
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom)
|
||||
|
||||
var statusSize = CGSize()
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
|
||||
var layoutWidth = imageLayoutSize.width
|
||||
if case .constrained = sizeCalculation {
|
||||
layoutWidth = max(layoutWidth, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right)
|
||||
}
|
||||
let layoutWidth = imageLayoutSize.width
|
||||
|
||||
let layoutSize = CGSize(width: layoutWidth, height: imageLayoutSize.height)
|
||||
|
||||
@@ -262,24 +266,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
transition.updateFrame(node: strongSelf.interactiveImageNode, frame: imageFrame)
|
||||
|
||||
if let statusApply = statusApply {
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.interactiveImageNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
var hasAnimation = true
|
||||
if case .None = animation {
|
||||
hasAnimation = false
|
||||
}
|
||||
statusApply(hasAnimation)
|
||||
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
|
||||
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
||||
strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
imageApply(transition, synchronousLoads)
|
||||
|
||||
if let selection = selection {
|
||||
@@ -310,14 +296,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
strongSelf.dateAndStatusNode.pressed = {
|
||||
strongSelf.interactiveImageNode.dateAndStatusNode.pressed = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
|
||||
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.interactiveImageNode.dateAndStatusNode)
|
||||
}
|
||||
} else {
|
||||
strongSelf.dateAndStatusNode.pressed = nil
|
||||
strongSelf.interactiveImageNode.dateAndStatusNode.pressed = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -356,7 +342,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.interactiveImageNode.isHidden = mediaHidden
|
||||
self.interactiveImageNode.updateIsHidden(mediaHidden)
|
||||
|
||||
if let automaticPlayback = self.automaticPlayback {
|
||||
/*if let automaticPlayback = self.automaticPlayback {
|
||||
if !automaticPlayback {
|
||||
self.dateAndStatusNode.isHidden = false
|
||||
} else if self.dateAndStatusNode.isHidden != mediaHidden {
|
||||
@@ -367,7 +353,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return mediaHidden
|
||||
}
|
||||
@@ -416,9 +402,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
/*if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
}*/
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import TelegramStringFormatting
|
||||
public final class ChatMessageNotificationItem: NotificationItem {
|
||||
let context: AccountContext
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let nameDisplayOrder: PresentationPersonNameOrder
|
||||
let messages: [Message]
|
||||
let tapAction: () -> Bool
|
||||
@@ -27,9 +28,10 @@ public final class ChatMessageNotificationItem: NotificationItem {
|
||||
return messages.first?.id.peerId
|
||||
}
|
||||
|
||||
public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) {
|
||||
public init(context: AccountContext, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.messages = messages
|
||||
self.tapAction = tapAction
|
||||
@@ -181,7 +183,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
if message.containsSecretMedia {
|
||||
imageDimensions = nil
|
||||
}
|
||||
messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, accountPeerId: item.context.account.peerId).0
|
||||
messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId).0
|
||||
} else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] {
|
||||
var displayAuthor = true
|
||||
if let channel = peer as? TelegramChannel {
|
||||
@@ -218,9 +220,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
}
|
||||
}
|
||||
} else if item.messages[0].groupingKey != nil {
|
||||
var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId).key
|
||||
var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId).key
|
||||
for i in 1 ..< item.messages.count {
|
||||
let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
|
||||
if kind != nextKind.key {
|
||||
kind = .text
|
||||
break
|
||||
|
||||
@@ -65,7 +65,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: context.account.peerId)
|
||||
let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId)
|
||||
|
||||
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
let titleColor: UIColor
|
||||
|
||||
@@ -21,6 +21,7 @@ private let inlineBotNameFont = nameFont
|
||||
class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
let imageNode: TransformImageNode
|
||||
private var placeholderNode: StickerShimmerEffectNode
|
||||
var textNode: TextNode?
|
||||
@@ -53,6 +54,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
@@ -119,7 +121,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
@@ -135,6 +138,23 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
item.controllerInteraction.openMessageReactions(item.message.id)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.activate = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -655,9 +675,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
dateAndStatusApply(false)
|
||||
|
||||
@@ -86,7 +86,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let contentNodeLayout = self.contentNode.asyncLayout()
|
||||
|
||||
return { item, layoutConstants, _, _, constrainedSize in
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
var webPage: TelegramMediaWebpage?
|
||||
var webPageContent: TelegramMediaWebpageLoadedContent?
|
||||
for media in item.message.media {
|
||||
@@ -301,7 +301,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, constrainedSize)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, preparePosition, constrainedSize)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
self.currentMessage = interfaceState.pinnedMessage
|
||||
|
||||
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
|
||||
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
|
||||
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,14 +314,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
self.currentLayout = (width, leftInset, rightInset)
|
||||
|
||||
if let currentMessage = self.currentMessage {
|
||||
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
|
||||
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
|
||||
}
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
|
||||
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
|
||||
let message = pinnedMessage.message
|
||||
|
||||
var animationTransition: ContainedViewLayoutTransition = .immediate
|
||||
@@ -470,7 +470,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
}
|
||||
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
||||
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
|
||||
@@ -260,6 +260,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
self?.openPeerMention(name)
|
||||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in
|
||||
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
|
||||
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in
|
||||
|
||||
@@ -262,12 +262,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
|
||||
}
|
||||
}
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
private func updateButtonTitle() {
|
||||
guard let date = self.pickerView?.date else {
|
||||
return
|
||||
}
|
||||
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
switch mode {
|
||||
case .scheduledMessages:
|
||||
|
||||
@@ -109,7 +109,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
|
||||
|
||||
self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
|
||||
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,7 @@ import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
|
||||
final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let messageId: MessageId
|
||||
|
||||
let closeButton: ASButtonNode
|
||||
@@ -67,12 +68,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||
var strings: PresentationStrings
|
||||
var nameDisplayOrder: PresentationPersonNameOrder
|
||||
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
|
||||
self.context = context
|
||||
self.messageId = messageId
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
|
||||
self.closeButton = ASButtonNode()
|
||||
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
|
||||
@@ -159,7 +161,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||
if let currentEditMediaReference = self.currentEditMediaReference {
|
||||
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
||||
}
|
||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, accountPeerId: self.context.account.peerId)
|
||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId)
|
||||
}
|
||||
|
||||
var updatedMediaReference: AnyMediaReference?
|
||||
@@ -231,7 +233,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||
if let currentEditMediaReference = self.currentEditMediaReference {
|
||||
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
||||
}
|
||||
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) {
|
||||
case .text:
|
||||
isMedia = false
|
||||
default:
|
||||
|
||||
@@ -70,6 +70,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, openPeer: { _, _, _ in
|
||||
}, openPeerMention: { _ in
|
||||
}, openMessageContextMenu: { _, _, _, _, _ in
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in
|
||||
}, navigateToMessageStandalone: { _ in
|
||||
|
||||
@@ -1019,7 +1019,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
displayLeave = false
|
||||
}
|
||||
result.append(.mute)
|
||||
if hasVoiceChat {
|
||||
if hasVoiceChat || canStartVoiceChat {
|
||||
result.append(.voiceChat)
|
||||
}
|
||||
if hasDiscussion {
|
||||
@@ -1038,7 +1038,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
|
||||
canReport = false
|
||||
}
|
||||
if !canReport && !canViewStats && !canStartVoiceChat {
|
||||
if !canReport && !canViewStats {
|
||||
displayMore = false
|
||||
}
|
||||
if displayMore {
|
||||
@@ -1051,10 +1051,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
var isPublic = false
|
||||
var isCreator = false
|
||||
var hasVoiceChat = false
|
||||
var canStartVoiceChat = false
|
||||
|
||||
if group.flags.contains(.hasVoiceChat) {
|
||||
hasVoiceChat = true
|
||||
}
|
||||
if !hasVoiceChat {
|
||||
if case .creator = group.role {
|
||||
canStartVoiceChat = true
|
||||
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) {
|
||||
canStartVoiceChat = true
|
||||
}
|
||||
}
|
||||
|
||||
if case .creator = group.role {
|
||||
isCreator = true
|
||||
@@ -1073,13 +1081,11 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if !group.hasBannedPermission(.banAddMembers) {
|
||||
canAddMembers = true
|
||||
}
|
||||
|
||||
if canAddMembers {
|
||||
result.append(.addMember)
|
||||
}
|
||||
|
||||
result.append(.mute)
|
||||
if hasVoiceChat {
|
||||
if hasVoiceChat || canStartVoiceChat {
|
||||
result.append(.voiceChat)
|
||||
}
|
||||
result.append(.search)
|
||||
|
||||
@@ -153,6 +153,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
||||
colors = ["Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor]
|
||||
if previousIcon == .unmute {
|
||||
playOnce = true
|
||||
@@ -164,6 +165,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
||||
colors = ["Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor]
|
||||
if previousIcon == .mute {
|
||||
playOnce = true
|
||||
@@ -248,7 +250,9 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
||||
if isActiveUpdated, !self.containerNode.alpha.isZero {
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3)
|
||||
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
|
||||
if !isExpanded {
|
||||
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||
|
||||
@@ -1848,6 +1848,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
|
||||
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { [weak self] message, node, rect, gesture in
|
||||
guard let strongSelf = self else {
|
||||
gesture?.cancel()
|
||||
@@ -3371,7 +3372,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
case .videoCall:
|
||||
self.requestCall(isVideo: true)
|
||||
case .voiceChat:
|
||||
self.requestCall(isVideo: false)
|
||||
self.requestCall(isVideo: false, gesture: gesture)
|
||||
case .mute:
|
||||
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
||||
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
||||
@@ -3627,20 +3628,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if !channel.flags.contains(.hasVoiceChat) {
|
||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, f in
|
||||
self?.requestCall(isVideo: false, contextController: c, result: f, backAction: { c in
|
||||
if let mainItemsImpl = mainItemsImpl {
|
||||
c.setItems(mainItemsImpl())
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
|
||||
@@ -3730,22 +3717,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
var canManageGroupCalls = false
|
||||
if case .creator = group.role {
|
||||
canManageGroupCalls = true
|
||||
} else if case let .admin(rights, _) = group.role {
|
||||
if rights.rights.contains(.canManageCalls) {
|
||||
canManageGroupCalls = true
|
||||
}
|
||||
}
|
||||
if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, f in
|
||||
self?.requestCall(isVideo: false, contextController: c, result: f)
|
||||
})))
|
||||
}
|
||||
|
||||
if case .Member = group.membership {
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
@@ -3976,14 +3947,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}
|
||||
}, activeCall: activeCall)
|
||||
} else {
|
||||
if let defaultJoinAsPeerId = defaultJoinAsPeerId {
|
||||
result?(.dismissWithoutContent)
|
||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
|
||||
} else {
|
||||
self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId)
|
||||
}, gesture: gesture, contextController: contextController, result: result, backAction: backAction)
|
||||
}
|
||||
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: gesture, contextController: contextController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4006,6 +3970,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
|
||||
}
|
||||
|
||||
private func scheduleGroupCall() {
|
||||
self.context.scheduleGroupCall(peerId: self.peerId)
|
||||
//
|
||||
//
|
||||
// let time = Int32(Date().timeIntervalSince1970 + 86400)
|
||||
// self.activeActionDisposable.set((createGroupCall(account: self.context.account, peerId: self.peerId, title: nil, scheduleDate: time)
|
||||
// |> deliverOnMainQueue).start(next: { [weak self] info in
|
||||
//
|
||||
// }))
|
||||
}
|
||||
|
||||
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
|
||||
if let _ = self.context.sharedContext.callManager {
|
||||
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
|
||||
@@ -4013,26 +3988,40 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
return
|
||||
}
|
||||
|
||||
var dismissStatus: (() -> Void)?
|
||||
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
|
||||
dismissStatus?()
|
||||
}))
|
||||
dismissStatus = { [weak self, weak statusController] in
|
||||
self?.activeActionDisposable.set(nil)
|
||||
statusController?.dismiss()
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.presentationData
|
||||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.controller?.present(statusController, in: .window(.root))
|
||||
strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId)
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
let createSignal = createGroupCall(account: strongSelf.context.account, peerId: peerId, title: nil, scheduleDate: nil)
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = { [weak self] in
|
||||
self?.activeActionDisposable.set(nil)
|
||||
}
|
||||
strongSelf.activeActionDisposable.set((createSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in
|
||||
result(joinAsPeerId)
|
||||
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title))
|
||||
}, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: nil, subscribed: false))
|
||||
}, error: { [weak self] error in
|
||||
dismissStatus?()
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@@ -4046,8 +4035,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||||
}
|
||||
strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
dismissStatus?()
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4348,7 +4335,90 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
controller.push(statsController)
|
||||
}
|
||||
|
||||
private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextController? = nil) {
|
||||
let context = self.context
|
||||
let peerId = self.peerId
|
||||
let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId
|
||||
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||
|> map { peer in
|
||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||
}
|
||||
let _ = (combineLatest(queue: Queue.mainQueue(), currentAccountPeer, self.displayAsPeersPromise.get() |> take(1))
|
||||
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
|
||||
var result = currentAccountPeer
|
||||
result.append(contentsOf: availablePeers)
|
||||
return result
|
||||
}).start(next: { [weak self] peers in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if peers.count > 1 {
|
||||
var selectedPeer: FoundPeer?
|
||||
for peer in peers {
|
||||
if peer.peer.id == defaultJoinAsPeerId {
|
||||
selectedPeer = peer
|
||||
}
|
||||
}
|
||||
if let peer = selectedPeer {
|
||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in
|
||||
let _ = updateGroupCallJoinAsPeer(account: context.account, peerId: peerId, joinAs: joinAsPeerId).start()
|
||||
self?.openVoiceChatOptions(defaultJoinAsPeerId: joinAsPeerId, gesture: nil, contextController: c)
|
||||
}, gesture: gesture, contextController: c, result: f, backAction: { [weak self] c in
|
||||
self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: nil, contextController: c)
|
||||
})
|
||||
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_ScheduleVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
self?.scheduleGroupCall()
|
||||
})))
|
||||
|
||||
if let contextController = contextController {
|
||||
contextController.setItems(.single(items))
|
||||
} else {
|
||||
strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat)
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
|
||||
if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller {
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
contextController.dismissed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.state = strongSelf.state.withHighlightedButton(nil)
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) {
|
||||
let dismissOnSelection = contextController == nil
|
||||
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map { peer in
|
||||
return [FoundPeer(peer: peer, subscribers: nil)]
|
||||
@@ -4398,8 +4468,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|
||||
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
if dismissOnSelection {
|
||||
f(.dismissWithoutContent)
|
||||
}
|
||||
completion(peer.peer.id)
|
||||
})))
|
||||
|
||||
@@ -7168,7 +7239,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController
|
||||
}
|
||||
|
||||
contactsController?.dismiss()
|
||||
},completed: {
|
||||
}, completed: {
|
||||
contactsController?.dismiss()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -29,7 +29,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
|
||||
var theme: PresentationTheme
|
||||
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
|
||||
self.messageId = messageId
|
||||
|
||||
self.theme = theme
|
||||
@@ -86,7 +86,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||
}
|
||||
if let message = message {
|
||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId)
|
||||
(text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
||||
}
|
||||
|
||||
var updatedMediaReference: AnyMediaReference?
|
||||
@@ -152,7 +152,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
|
||||
let isMedia: Bool
|
||||
if let message = message {
|
||||
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) {
|
||||
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) {
|
||||
case .text:
|
||||
isMedia = false
|
||||
default:
|
||||
|
||||
@@ -1220,7 +1220,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
let controllerInteraction: ChatControllerInteraction
|
||||
if tapMessage != nil || clickThroughMessage != nil {
|
||||
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
|
||||
}, tapMessage: { message in
|
||||
tapMessage?(message)
|
||||
}, clickThroughMessage: {
|
||||
|
||||
Submodule submodules/TgVoipWebrtc/tgcalls updated: e8e949c07c...40fc820cc9
Reference in New Issue
Block a user