diff --git a/Random.txt b/Random.txt index ca2dee0413..9a9364694b 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -8tRwQybvfoDddhSIfdMSOMv4FZd9LSHiWmObmx6d7rE= +gCh0ST/jBZ+NM8mvcBcsd12A5FMFT4q6fETcWd5elO0= diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 8169ca55dc..8a85ac068f 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -22,15 +22,20 @@ public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId { } public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation { - case messages(peerId: PeerId, tagMask: MessageTags, at: MessageId) + case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId) case singleMessage(MessageId) case recentActions(Message) case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?) public var playlistId: PeerMessagesMediaPlaylistId { switch self { - case let .messages(peerId, _, _): - return .peer(peerId) + case let .messages(chatLocation, _, _): + switch chatLocation { + case let .peer(peerId): + return .peer(peerId) + case let .replyThread(replyThreaMessage): + return .peer(replyThreaMessage.messageId.peerId) + } case let .singleMessage(id): return .peer(id.peerId) case let .recentActions(message): @@ -59,8 +64,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool { switch lhs { - case let .messages(peerId, tagMask, at): - if case .messages(peerId, tagMask, at) = rhs { + case let .messages(chatLocation, tagMask, at): + if case .messages(chatLocation, tagMask, at) = rhs { return true } else { return false diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 08b8fe32a1..1d6001ac45 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -13,6 +13,7 @@ import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext +import TelegramCallsUI @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { @@ -74,6 +75,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case alternativeFolderTabs(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) + case voiceConference case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) case enableVoipTcp(Bool) @@ -90,7 +92,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback: + case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback, .voiceConference: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -155,8 +157,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 24 case .playlistPlayback: return 25 + case .voiceConference: + return 26 case let .preferredVideoCodec(index, _, _, _): - return 26 + index + return 27 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -648,6 +652,15 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case .voiceConference: + return ItemListDisclosureItem(presentationData: presentationData, title: "Voice Conference (Test)", label: "", sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + let controller = GroupCallController(context: context) + controller.navigationPresentation = .modal + arguments.pushController(controller) + }) case let .preferredVideoCodec(_, title, value, isSelected): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -725,6 +738,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) + entries.append(.voiceConference) + let codecs: [(String, String?)] = [ ("No Preference", nil), ("H265", "H265"), diff --git a/submodules/TelegramCallsUI/Sources/GroupCallController.swift b/submodules/TelegramCallsUI/Sources/GroupCallController.swift new file mode 100644 index 0000000000..ae870995de --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/GroupCallController.swift @@ -0,0 +1,147 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import TelegramVoip +import TelegramAudio +import AccountContext + +public final class GroupCallController: ViewController { + private final class Node: ViewControllerTracingNode { + private let context: AccountContext + private let presentationData: PresentationData + + private var callContext: GroupCallContext? + private var callDisposable: Disposable? + private var memberCountDisposable: Disposable? + private var isMutedDisposable: Disposable? + private let audioSessionActive = Promise(false) + + private var memberCount: Int = 0 + private let memberCountNode: ImmediateTextNode + + private var isMuted: Bool = false + private let isMutedNode: ImmediateTextNode + private let muteButton: HighlightableButtonNode + + private var validLayout: ContainerViewLayout? + + init(context: AccountContext) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.memberCountNode = ImmediateTextNode() + self.isMutedNode = ImmediateTextNode() + + self.muteButton = HighlightableButtonNode() + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.memberCountNode) + + self.muteButton.addSubnode(self.isMutedNode) + self.addSubnode(self.muteButton) + + let audioSessionActive = self.audioSessionActive + self.callDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .voiceCall, manualActivate: { audioSessionControl in + audioSessionControl.activate({ _ in }) + audioSessionActive.set(.single(true)) + }, deactivate: { + return Signal { subscriber in + subscriber.putCompletion() + return EmptyDisposable + } + }, availableOutputsChanged: { _, _ in + }) + + let callContext = GroupCallContext(audioSessionActive: self.audioSessionActive.get()) + self.callContext = callContext + + self.memberCountDisposable = (callContext.memberCount + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.memberCount = value + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + } + }) + + self.isMutedDisposable = (callContext.isMuted + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.isMuted = value + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + } + }) + + self.muteButton.addTarget(self, action: #selector(self.muteButtonPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.callDisposable?.dispose() + self.memberCountDisposable?.dispose() + } + + @objc private func muteButtonPressed() { + self.callContext?.toggleIsMuted() + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + self.memberCountNode.attributedText = NSAttributedString(string: "Members: \(self.memberCount)", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + + self.isMutedNode.attributedText = NSAttributedString(string: self.isMuted ? "Unmute" : "Mute", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor) + + let textSize = self.memberCountNode.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: 100.0)) + let isMutedSize = self.isMutedNode.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: 100.0)) + + let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: floor((layout.size.height - textSize.width) / 2.0)), size: textSize) + transition.updateFrameAdditiveToCenter(node: self.memberCountNode, frame: textFrame) + + let isMutedFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - isMutedSize.width) / 2.0), y: textFrame.maxY + 12.0), size: isMutedSize) + transition.updateFrame(node: self.muteButton, frame: isMutedFrame) + self.isMutedNode.frame = CGRect(origin: CGPoint(), size: isMutedFrame.size) + } + } + + private let context: AccountContext + private let presentationData: PresentationData + + private var controllerNode: Node { + return self.displayNode as! Node + } + + public init(context: AccountContext) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(context: self.context) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 06ca99a200..6e511d83a6 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -170,6 +170,10 @@ public final class PrincipalThemeEssentialGraphics { public let outgoingDateAndStatusRepliesIcon: UIImage public let mediaRepliesIcon: UIImage public let freeRepliesIcon: UIImage + public let incomingDateAndStatusPinnedIcon: UIImage + public let outgoingDateAndStatusPinnedIcon: UIImage + public let mediaPinnedIcon: UIImage + public let freePinnedIcon: UIImage public let dateStaticBackground: UIImage public let dateFloatingBackground: UIImage @@ -333,6 +337,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! + self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaPinnedIcon = generateTintedImage(image: pinnedImage, color: .white)! + self.freePinnedIcon = generateTintedImage(image: pinnedImage, color: serviceColor.primaryText)! + self.radialIndicatorFileIconIncoming = emptyImage self.radialIndicatorFileIconOutgoing = emptyImage } else { @@ -438,6 +448,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! + self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaPinnedIcon = generateTintedImage(image: pinnedImage, color: .white)! + self.freePinnedIcon = generateTintedImage(image: pinnedImage, color: serviceColor.primaryText)! + self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/Contents.json index 38f0c81fc2..6e965652df 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/Contents.json new file mode 100644 index 0000000000..1868f3d835 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "messagepin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/messagepin.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/messagepin.pdf new file mode 100644 index 0000000000..a42a8d6261 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/messagepin.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ef4e3c96fd..4f6929aaac 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -358,6 +358,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G public var purposefulAction: (() -> Void)? + private let scrolledToMessageId = ValuePromise(nil, ignoreRepeated: true) + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -3258,11 +3260,36 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) |> restartIfError + struct ReferenceMessage { + var id: MessageId + var isScrolled: Bool + } + + let referenceMessage: Signal + if latest { + referenceMessage = .single(nil) + } else { + referenceMessage = combineLatest( + queue: Queue.mainQueue(), + self.scrolledToMessageId.get(), + self.chatDisplayNode.historyNode.topVisibleMessageRange.get() + ) + |> map { scrolledToMessageId, topVisibleMessageRange -> ReferenceMessage? in + if let scrolledToMessageId = scrolledToMessageId { + return ReferenceMessage(id: scrolledToMessageId, isScrolled: true) + } else if let topVisibleMessageRange = topVisibleMessageRange { + return ReferenceMessage(id: topVisibleMessageRange.upperBound, isScrolled: false) + } else { + return nil + } + } + } + topPinnedMessage = combineLatest( replyHistory, - latest ? .single(nil) : self.chatDisplayNode.historyNode.topVisibleMessageRange.get() + referenceMessage ) - |> map { update, topVisibleMessageRange -> ChatPinnedMessage? in + |> map { update, referenceMessage -> ChatPinnedMessage? in var message: ChatPinnedMessage? switch update { case .Loading: @@ -3278,9 +3305,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var matches = false if message == nil { matches = true - } else if let topVisibleMessageRange = topVisibleMessageRange { - if entry.message.id <= topVisibleMessageRange.upperBound { - matches = true + } else if let referenceMessage = referenceMessage { + if referenceMessage.isScrolled { + if entry.message.id < referenceMessage.id { + matches = true + } + } else { + if entry.message.id <= referenceMessage.id { + matches = true + } } } else { matches = true @@ -3318,6 +3351,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + self.chatDisplayNode.historyNode.beganDragging = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.scrolledToMessageId.set(nil) + } + self.chatDisplayNode.peerView = self.peerView let initialData = self.chatDisplayNode.historyNode.initialData @@ -3645,6 +3686,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: false) + strongSelf.scrolledToMessageId.set(index.id) strongSelf.messageContextDisposable.set((Signal.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { @@ -4874,7 +4916,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, unpinMessage: { [weak self] id in + }, unpinMessage: { [weak self] id, askForConfirmation in if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { var canManagePin = false @@ -4896,7 +4938,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if canManagePin { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { + let action: () -> Void = { + guard let strongSelf = self else { + return + } if let strongSelf = self { let disposable: MetaDisposable if let current = strongSelf.unpinMessageDisposable { @@ -4907,7 +4952,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } disposable.set(requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)).start()) } - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) + } + if askForConfirmation { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { + action() + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) + } else { + action() + } } else { if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 31f8d7b146..f0669a5294 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -526,6 +526,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? + var beganDragging: (() -> Void)? private let hasVisiblePlayableItemNodesPromise = ValuePromise(false, ignoreRepeated: true) var hasVisiblePlayableItemNodes: Signal { @@ -1121,6 +1122,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.beganInteractiveDragging = { [weak self] in self?.isInteractivelyScrollingValue = true self?.isInteractivelyScrollingPromise.set(true) + self?.beganDragging?() } self.didEndScrolling = { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 572651d5fa..c85b768e66 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -664,7 +664,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - interfaceInteraction.unpinMessage(pinnedSelectedMessageId) + interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false) f(.default) }))) } else { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index d98636193e..cd0f124341 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -702,7 +702,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount) - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned)) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 65f3007e70..1669a48363 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -572,7 +572,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) + statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned)) } default: break diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index d1066e6b7b..8328e9d8a1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -609,9 +609,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let strongSelf = self { for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? - let convertedNodeFrame = contentNode.convert(contentNode.bounds, to: strongSelf) + let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view) if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.convert(point, to: contentNode) + translatedPoint = strongSelf.view.convert(point, to: contentNode.view) } contentNode.updateTouchesAtPoint(translatedPoint) } @@ -905,7 +905,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), - mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), + mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), currentShareButtonNode: HighlightableButtonNode?, layoutConstants: ChatMessageItemLayoutConstants, currentItem: ChatMessageItem?, @@ -1442,7 +1442,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned)) } } @@ -2873,7 +2873,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } loop: for contentNode in self.contentNodes { - let convertedLocation = self.convert(location, to: contentNode) + let convertedLocation = self.view.convert(location, to: contentNode.view) let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) switch tapAction { @@ -2982,9 +2982,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var tapMessage: Message? = item.content.firstMessage var selectAll = true loop: for contentNode in self.contentNodes { - let convertedLocation = self.convert(location, to: contentNode) + let convertedLocation = self.view.convert(location, to: contentNode.view) - let convertedNodeFrame = contentNode.convert(contentNode.bounds, to: self) + let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: self.view) if !convertedNodeFrame.contains(location) { continue loop } else if contentNode is ChatMessageMediaBubbleContentNode { diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index b907849260..3f21ad5679 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -196,7 +196,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 5086a30fbe..5552dcb790 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -180,7 +180,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.addSubnode(self.dateNode) } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int) -> (CGSize, (Bool) -> Void) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -200,7 +200,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let previousLayoutSize = self.layoutSize - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount in + return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned in let dateColor: UIColor var backgroundImage: UIImage? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? @@ -234,6 +234,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.incomingDateAndStatusRepliesIcon + } else if isPinned { + repliesImage = graphics.incomingDateAndStatusPinnedIcon } case let .BubbleOutgoing(status): dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor @@ -248,6 +250,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.outgoingDateAndStatusRepliesIcon + } else if isPinned { + repliesImage = graphics.outgoingDateAndStatusPinnedIcon } case .ImageIncoming: dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -262,6 +266,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.mediaRepliesIcon + } else if isPinned { + repliesImage = graphics.mediaPinnedIcon } case let .ImageOutgoing(status): dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -277,6 +283,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.mediaRepliesIcon + } else if isPinned { + repliesImage = graphics.mediaPinnedIcon } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) @@ -292,6 +300,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.freeRepliesIcon + } else if isPinned { + repliesImage = graphics.freePinnedIcon } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) @@ -308,6 +318,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if replyCount != 0 { repliesImage = graphics.freeRepliesIcon + } else if isPinned { + repliesImage = graphics.freePinnedIcon } } @@ -511,6 +523,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 replyCountLayoutAndApply = layoutAndApply + } else if isPinned { + reactionInset += 12.0 } leftInset += reactionInset @@ -817,17 +831,17 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { + static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { let currentLayout = node?.asyncLayout() - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies in + return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned in let resultNode: ChatMessageDateAndStatusNode let resultSizeAndApply: (CGSize, (Bool) -> Void) if let node = node, let currentLayout = currentLayout { resultNode = node - resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies) + resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned) } else { resultNode = ChatMessageDateAndStatusNode() - resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies) + resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned) } return (resultSizeAndApply.0, { animated in diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 62597c088f..b79a9283c7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -327,7 +327,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount) - let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies) + let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 17b75280d3..5322a32a25 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -285,7 +285,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { maxDateAndStatusWidth = width - videoFrame.midX - 85.0 } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned)) var contentSize = imageSize var dateAndStatusOverflow = false diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 9996545f0b..eedf4a2ff7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -246,7 +246,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 9c43bddac1..be610a08fc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -231,7 +231,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + 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)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index ecbf4508b9..edc89c275e 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1074,7 +1074,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 424e01650a..52959991cf 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -105,7 +105,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index a668db3247..b0c6f1b7f3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -382,7 +382,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount) - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned)) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index eccca0af19..0a5b9266ab 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -169,7 +169,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned)) statusSize = size statusApply = apply } diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index f0f1397f01..5e9a6ef85c 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -95,7 +95,7 @@ final class ChatPanelInterfaceInteraction { let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let unblockPeer: () -> Void let pinMessage: (MessageId) -> Void - let unpinMessage: (MessageId) -> Void + let unpinMessage: (MessageId, Bool) -> Void let shareAccountContact: () -> Void let reportPeer: () -> Void let presentPeerContact: () -> Void @@ -171,7 +171,7 @@ final class ChatPanelInterfaceInteraction { sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, - unpinMessage: @escaping (MessageId) -> Void, + unpinMessage: @escaping (MessageId, Bool) -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index fde33abda8..6babab3311 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -362,7 +362,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { @objc func closePressed() { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { - interfaceInteraction.unpinMessage(message.message.id) + interfaceInteraction.unpinMessage(message.message.id, true) } } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index 522c2b6b94..7a35c46b71 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController { return false }, unblockPeer: { }, pinMessage: { _ in - }, unpinMessage: { _ in + }, unpinMessage: { _, _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 48c6464cd4..835f7735b4 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -130,7 +130,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } else if params.standalone { location = .recentActions(params.message) } else { - location = .messages(peerId: params.message.id.peerId, tagMask: .voiceOrInstantVideo, at: params.message.id) + location = .messages(chatLocation: params.chatLocation ?? .peer(params.message.id.peerId), tagMask: .voiceOrInstantVideo, at: params.message.id) } playerType = .voice } else if file.isMusic && params.message.tags.contains(.music) { @@ -139,7 +139,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } else if params.standalone { location = .recentActions(params.message) } else { - location = .messages(peerId: params.message.id.peerId, tagMask: .music, at: params.message.id) + location = .messages(chatLocation: params.chatLocation ?? .peer(params.message.id.peerId), tagMask: .music, at: params.message.id) } playerType = .music } else { @@ -150,7 +150,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music } - params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(postbox: params.context.account.postbox, network: params.context.account.network, location: location)), type: playerType, control: control) + params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control) return true case let .gallery(gallery): params.dismissInput() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 2fe33c23dd..f3470f6981 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -52,6 +52,7 @@ import PeerInfoUI import ListMessageItem import GalleryData import ChatInterfaceState +import TelegramVoip protocol PeerInfoScreenItem: class { var id: AnyHashable { get } @@ -405,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { return false }, unblockPeer: { }, pinMessage: { _ in - }, unpinMessage: { _ in + }, unpinMessage: { _, _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { @@ -3153,6 +3154,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.present(shareController, in: .window(.root)) } + private let groupCallDisposable = MetaDisposable() + private var groupCall: GroupCallContext? + private func requestCall(isVideo: Bool) { guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { return diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index f38171f3fe..87053a638e 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -307,9 +307,9 @@ private struct PlaybackStack { } final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { - private let postbox: Postbox - private let network: Network + private let context: AccountContext private let messagesLocation: PeerMessagesPlaylistLocation + private let chatLocationContextHolder: Atomic? var location: SharedMediaPlaylistLocation { return self.messagesLocation @@ -338,13 +338,13 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { return self.stateValue.get() } - init(postbox: Postbox, network: Network, location: PeerMessagesPlaylistLocation) { + init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic?) { assert(Queue.mainQueue().isCurrent()) self.id = location.playlistId - self.postbox = postbox - self.network = network + self.context = context + self.chatLocationContextHolder = chatLocationContextHolder self.messagesLocation = location switch self.messagesLocation { @@ -446,7 +446,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.currentlyObservedMessageId = item?.message.id if let id = item?.message.id { let key: PostboxViewKey = .messages(Set([id])) - self.currentlyObservedMessageDisposable.set((self.postbox.combinedView(keys: [key]) + self.currentlyObservedMessageDisposable.set((self.context.account.postbox.combinedView(keys: [key]) |> filter { views in if let view = views.views[key] as? MessagesView { if !view.messages.isEmpty { @@ -479,15 +479,15 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { switch anchor { case let .messageId(messageId): switch self.messagesLocation { - case let .messages(peerId, tagMask, _): - let historySignal = self.postbox.messageAtId(messageId) + case let .messages(chatLocation, tagMask, _): + let historySignal = self.context.account.postbox.messageAtId(messageId) |> take(1) |> mapToSignal { message -> Signal<(Message, [Message])?, NoError> in guard let message = message else { return .single(nil) } - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in if let (message, aroundMessages, _) = navigatedMessageFromView(view.0, anchorIndex: message.index, position: .exact) { return .single((message, aroundMessages)) @@ -539,7 +539,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } })) default: - self.navigationDisposable.set((self.postbox.messageAtId(messageId) + self.navigationDisposable.set((self.context.account.postbox.messageAtId(messageId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { @@ -559,7 +559,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } case let .index(index): switch self.messagesLocation { - case let .messages(peerId, tagMask, _): + case let .messages(chatLocation, tagMask, _): let inputIndex: Signal let looping = self.looping switch self.order { @@ -567,7 +567,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { inputIndex = .single(index) case .random: var playbackStack = self.playbackStack - inputIndex = self.postbox.transaction { transaction -> MessageIndex in + inputIndex = self.context.account.postbox.transaction { transaction -> MessageIndex in if case let .random(previous) = navigation, previous { let _ = playbackStack.pop() while true { @@ -580,12 +580,12 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } } - return transaction.findRandomMessage(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: tagMask, ignoreIds: (playbackStack.ids, playbackStack.set)) ?? index + return transaction.findRandomMessage(peerId: chatLocation.peerId, namespace: Namespaces.Message.Cloud, tag: tagMask, ignoreIds: (playbackStack.ids, playbackStack.set)) ?? index } } let historySignal = inputIndex |> mapToSignal { inputIndex -> Signal<(Message, [Message])?, NoError> in - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation { @@ -615,7 +615,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } else { viewIndex = .lowerBound } - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation { @@ -657,7 +657,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } })) case .singleMessage: - self.navigationDisposable.set((self.postbox.messageAtId(index.id) + self.navigationDisposable.set((self.context.account.postbox.messageAtId(index.id) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { @@ -815,7 +815,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { default: break } - let _ = markMessageContentAsConsumedInteractively(postbox: self.postbox, messageId: item.message.id).start() + let _ = markMessageContentAsConsumedInteractively(postbox: self.context.account.postbox, messageId: item.message.id).start() } } } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift new file mode 100644 index 0000000000..0e57845a60 --- /dev/null +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -0,0 +1,1469 @@ +import Foundation +import SwiftSignalKit +import TgVoipWebrtc + +private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc { + private let queue: Queue + + init(queue: Queue) { + self.queue = queue + + super.init() + } + + func dispatch(_ f: @escaping () -> Void) { + self.queue.async { + f() + } + } + + func dispatch(after seconds: Double, block f: @escaping () -> Void) { + self.queue.after(seconds, f) + } + + func isCurrent() -> Bool { + return self.queue.isCurrent() + } +} + +private struct ConferenceDescription { + struct Transport { + struct Candidate { + var id: String + var generation: Int + var component: String + var `protocol`: String + var tcpType: String? + var ip: String + var port: Int + var foundation: String + var priority: Int + var type: String + var network: Int + var relAddr: String? + var relPort: Int? + } + + struct Fingerprint { + var fingerprint: String + var setup: String + var hashType: String + } + + var candidates: [Candidate] + var fingerprints: [Fingerprint] + var ufrag: String + var pwd: String + } + + struct ChannelBundle { + var id: String + var transport: Transport + } + + struct Content { + struct Channel { + struct SsrcGroup { + var sources: [Int] + var semantics: String + } + + struct PayloadType { + var id: Int + var name: String + var clockrate: Int + var channels: Int + var parameters: [String: Any]? + } + + struct RtpHdrExt { + var id: Int + var uri: String + } + + var id: String? + var endpoint: String + var channelBundleId: String + var sources: [Int] + var ssrcs: [Int] + var rtpLevelRelayType: String + var expire: Int? + var initiator: Bool + var direction: String + var ssrcGroups: [SsrcGroup] + var payloadTypes: [PayloadType] + var rtpHdrExts: [RtpHdrExt] + } + + var name: String + var channels: [Channel] + } + + var id: String + var channelBundles: [ChannelBundle] + var contents: [Content] + + init?(json: [String: Any]) { + guard let id = json["id"] as? String else { + assert(false) + return nil + } + self.id = id + + var channelBundles: [ChannelBundle] = [] + if let channelBundlesJson = json["channel-bundles"] as? [Any] { + for channelBundleValue in channelBundlesJson { + if let channelBundleJson = channelBundleValue as? [String: Any] { + if let channelBundle = ChannelBundle(json: channelBundleJson) { + channelBundles.append(channelBundle) + } + } + } + } + self.channelBundles = channelBundles + + var contents: [Content] = [] + if let contentsJson = json["contents"] as? [Any] { + for contentValue in contentsJson { + if let contentJson = contentValue as? [String: Any] { + if let content = Content(json: contentJson) { + contents.append(content) + } + } + } + } + self.contents = contents + } +} + +private extension ConferenceDescription.Transport.Candidate { + init?(json: [String: Any]) { + guard let id = json["id"] as? String else { + assert(false) + return nil + } + self.id = id + + if let generationString = json["generation"] as? String, let generation = Int(generationString) { + self.generation = generation + } else { + self.generation = 0 + } + + guard let component = json["component"] as? String else { + assert(false) + return nil + } + self.component = component + + guard let `protocol` = json["protocol"] as? String else { + assert(false) + return nil + } + self.protocol = `protocol` + + if let tcpType = json["tcptype"] as? String { + self.tcpType = tcpType + } else { + self.tcpType = nil + } + + guard let ip = json["ip"] as? String else { + assert(false) + return nil + } + self.ip = ip + + guard let portString = json["port"] as? String, let port = Int(portString) else { + assert(false) + return nil + } + self.port = port + + guard let foundation = json["foundation"] as? String else { + assert(false) + return nil + } + self.foundation = foundation + + guard let priorityString = json["priority"] as? String, let priority = Int(priorityString) else { + assert(false) + return nil + } + self.priority = priority + + guard let type = json["type"] as? String else { + assert(false) + return nil + } + self.type = type + + guard let networkString = json["network"] as? String, let network = Int(networkString) else { + assert(false) + return nil + } + self.network = network + + if let relAddr = json["rel-addr"] as? String { + self.relAddr = relAddr + } else { + self.relAddr = nil + } + + if let relPortString = json["rel-port"] as? String, let relPort = Int(relPortString) { + self.relPort = relPort + } else { + self.relPort = nil + } + } +} + +private extension ConferenceDescription.Transport.Fingerprint { + init?(json: [String: Any]) { + guard let fingerprint = json["fingerprint"] as? String else { + assert(false) + return nil + } + self.fingerprint = fingerprint + + guard let setup = json["setup"] as? String else { + assert(false) + return nil + } + self.setup = setup + + guard let hashType = json["hash"] as? String else { + assert(false) + return nil + } + self.hashType = hashType + } +} + +private extension ConferenceDescription.Transport { + init?(json: [String: Any]) { + guard let ufrag = json["ufrag"] as? String else { + assert(false) + return nil + } + self.ufrag = ufrag + + guard let pwd = json["pwd"] as? String else { + assert(false) + return nil + } + self.pwd = pwd + + var candidates: [Candidate] = [] + if let candidatesJson = json["candidates"] as? [Any] { + for candidateValue in candidatesJson { + if let candidateJson = candidateValue as? [String: Any] { + if let candidate = Candidate(json: candidateJson) { + candidates.append(candidate) + } + } + } + } + self.candidates = candidates + + var fingerprints: [Fingerprint] = [] + if let fingerprintsJson = json["fingerprints"] as? [Any] { + for fingerprintValue in fingerprintsJson { + if let fingerprintJson = fingerprintValue as? [String: Any] { + if let fingerprint = Fingerprint(json: fingerprintJson) { + fingerprints.append(fingerprint) + } + } + } + } + self.fingerprints = fingerprints + } +} + +private extension ConferenceDescription.ChannelBundle { + init?(json: [String: Any]) { + guard let id = json["id"] as? String else { + assert(false) + return nil + } + self.id = id + + guard let transportJson = json["transport"] as? [String: Any] else { + assert(false) + return nil + } + guard let transport = ConferenceDescription.Transport(json: transportJson) else { + assert(false) + return nil + } + self.transport = transport + } +} + +private extension ConferenceDescription.Content.Channel.SsrcGroup { + init?(json: [String: Any]) { + guard let sources = json["sources"] as? [Int] else { + assert(false) + return nil + } + self.sources = sources + + guard let semantics = json["semantics"] as? String else { + assert(false) + return nil + } + self.semantics = semantics + } +} + +private extension ConferenceDescription.Content.Channel.PayloadType { + init?(json: [String: Any]) { + guard let idString = json["id"] as? String, let id = Int(idString) else { + assert(false) + return nil + } + self.id = id + + guard let name = json["name"] as? String else { + assert(false) + return nil + } + self.name = name + + guard let clockrateString = json["clockrate"] as? String, let clockrate = Int(clockrateString) else { + assert(false) + return nil + } + self.clockrate = clockrate + + guard let channelsString = json["channels"] as? String, let channels = Int(channelsString) else { + assert(false) + return nil + } + self.channels = channels + + self.parameters = json["parameters"] as? [String: Any] + } +} + +private extension ConferenceDescription.Content.Channel.RtpHdrExt { + init?(json: [String: Any]) { + guard let idString = json["id"] as? String, let id = Int(idString) else { + assert(false) + return nil + } + self.id = id + + guard let uri = json["uri"] as? String else { + assert(false) + return nil + } + self.uri = uri + } +} + +private extension ConferenceDescription.Content.Channel { + init?(json: [String: Any]) { + guard let id = json["id"] as? String else { + assert(false) + return nil + } + self.id = id + + guard let endpoint = json["endpoint"] as? String else { + assert(false) + return nil + } + self.endpoint = endpoint + + guard let channelBundleId = json["channel-bundle-id"] as? String else { + assert(false) + return nil + } + self.channelBundleId = channelBundleId + + guard let sources = json["sources"] as? [Int] else { + assert(false) + return nil + } + self.sources = sources + + if let ssrcs = json["ssrcs"] as? [Int] { + self.ssrcs = ssrcs + } else { + self.ssrcs = [] + } + + guard let rtpLevelRelayType = json["rtp-level-relay-type"] as? String else { + assert(false) + return nil + } + self.rtpLevelRelayType = rtpLevelRelayType + + if let expire = json["expire"] as? Int { + self.expire = expire + } else { + self.expire = nil + } + + guard let initiator = json["initiator"] as? Bool else { + assert(false) + return nil + } + self.initiator = initiator + + guard let direction = json["direction"] as? String else { + assert(false) + return nil + } + self.direction = direction + + var ssrcGroups: [SsrcGroup] = [] + if let ssrcGroupsJson = json["ssrc-groups"] as? [Any] { + for ssrcGroupValue in ssrcGroupsJson { + if let ssrcGroupJson = ssrcGroupValue as? [String: Any] { + if let ssrcGroup = SsrcGroup(json: ssrcGroupJson) { + ssrcGroups.append(ssrcGroup) + } + } + } + } + self.ssrcGroups = ssrcGroups + + var payloadTypes: [PayloadType] = [] + if let payloadTypesJson = json["payload-types"] as? [Any] { + for payloadTypeValue in payloadTypesJson { + if let payloadTypeJson = payloadTypeValue as? [String: Any] { + if let payloadType = PayloadType(json: payloadTypeJson) { + payloadTypes.append(payloadType) + } + } + } + } + self.payloadTypes = payloadTypes + + var rtpHdrExts: [RtpHdrExt] = [] + if let rtpHdrExtsJson = json["rtp-hdrexts"] as? [Any] { + for rtpHdrExtValue in rtpHdrExtsJson { + if let rtpHdrExtJson = rtpHdrExtValue as? [String: Any] { + if let rtpHdrExt = RtpHdrExt(json: rtpHdrExtJson) { + rtpHdrExts.append(rtpHdrExt) + } + } + } + } + self.rtpHdrExts = rtpHdrExts + } +} + +private extension ConferenceDescription.Content { + init?(json: [String: Any]) { + guard let name = json["name"] as? String else { + assert(false) + return nil + } + self.name = name + + var channels: [Channel] = [] + if let channelsJson = json["channels"] as? [Any] { + for channelValue in channelsJson { + if let channelJson = channelValue as? [String: Any] { + if let channel = Channel(json: channelJson) { + channels.append(channel) + } + } + } + } + self.channels = channels + } +} + +private extension ConferenceDescription.Content.Channel.SsrcGroup { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["sources"] = self.sources + result["semantics"] = self.semantics + + return result + } +} + +private extension ConferenceDescription.Content.Channel.PayloadType { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["id"] = self.id + result["name"] = self.name + result["channels"] = self.channels + result["clockrate"] = self.clockrate + result["rtcp-fbs"] = [[ + "type": "transport-cc" + ] as [String: Any]] as [Any] + if let parameters = self.parameters { + result["parameters"] = parameters + } + + return result + } +} + +private extension ConferenceDescription.Content.Channel.RtpHdrExt { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["id"] = self.id + result["uri"] = self.uri + + return result + } +} + +private extension ConferenceDescription.Content.Channel { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + if let id = self.id { + result["id"] = id + } + result["expire"] = self.expire ?? 10 + result["initiator"] = self.initiator + result["endpoint"] = self.endpoint + result["direction"] = self.direction + result["channel-bundle-id"] = self.channelBundleId + result["rtp-level-relay-type"] = self.rtpLevelRelayType + if !self.sources.isEmpty { + result["sources"] = self.sources + } + if !self.ssrcs.isEmpty { + result["ssrcs"] = self.ssrcs + } + if !self.ssrcGroups.isEmpty { + result["ssrc-groups"] = self.ssrcGroups.map { $0.outgoingColibriDescription() } + } + if !self.payloadTypes.isEmpty { + result["payload-types"] = self.payloadTypes.map { $0.outgoingColibriDescription() } + } + if !self.rtpHdrExts.isEmpty { + result["rtp-hdrexts"] = self.rtpHdrExts.map { $0.outgoingColibriDescription() } + } + result["rtcp-mux"] = true + + return result + } +} + +private extension ConferenceDescription.Content { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["name"] = self.name + result["channels"] = self.channels.map { $0.outgoingColibriDescription() } + + return result + } +} + +private extension ConferenceDescription.Transport.Fingerprint { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["fingerprint"] = self.fingerprint + result["setup"] = self.setup + result["hash"] = self.hashType + + return result + } +} + +private extension ConferenceDescription.Transport.Candidate { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["id"] = self.id + result["generation"] = self.generation + result["component"] = self.component + result["protocol"] = self.protocol + if let tcpType = self.tcpType { + result["tcptype"] = tcpType + } + result["ip"] = self.ip + result["port"] = self.port + result["foundation"] = self.foundation + result["priority"] = self.priority + result["type"] = self.type + result["network"] = self.network + if let relAddr = self.relAddr { + result["rel-addr"] = relAddr + } + if let relPort = self.relPort { + result["rel-port"] = relPort + } + + return result + } +} + +private extension ConferenceDescription.Transport { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["xmlns"] = "urn:xmpp:jingle:transports:ice-udp:1" + result["rtcp-mux"] = true + + if !self.ufrag.isEmpty { + result["ufrag"] = self.ufrag + result["pwd"] = self.pwd + } + + if !self.fingerprints.isEmpty { + result["fingerprints"] = self.fingerprints.map { $0.outgoingColibriDescription() } + } + + if !self.candidates.isEmpty { + result["candidates"] = self.candidates.map { $0.outgoingColibriDescription() } + } + + return result + } +} + +private extension ConferenceDescription.ChannelBundle { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["id"] = self.id + result["transport"] = self.transport.outgoingColibriDescription() + + return result + } +} + +private struct RemoteOffer { + struct State: Equatable { + struct Item: Equatable { + var ssrc: Int + var isRemoved: Bool + } + + var items: [Item] + } + + var sdpList: [String] + var isPartial: Bool + var state: State +} + +private extension ConferenceDescription { + func outgoingColibriDescription() -> [String: Any] { + var result: [String: Any] = [:] + + result["id"] = self.id + result["contents"] = self.contents.map { $0.outgoingColibriDescription() } + result["channel-bundles"] = self.channelBundles.map { $0.outgoingColibriDescription() } + + return result + } + + func offerSdp(sessionId: UInt32, bundleId: String, bridgeHost: String, transport: ConferenceDescription.Transport, currentState: RemoteOffer.State?) -> RemoteOffer? { + struct Ssrc { + var isMain: Bool + var value: Int + var streamId: String + var isRemoved: Bool + } + + func createSdp(sessionId: UInt32, bundleSsrcs: [Ssrc], isPartial: Bool) -> String { + var sdp = "" + func appendSdp(_ string: String) { + if !sdp.isEmpty { + sdp.append("\n") + } + sdp.append(string) + } + + appendSdp("v=0") + appendSdp("o=- \(sessionId) 2 IN IP4 0.0.0.0") + appendSdp("s=-") + appendSdp("t=0 0") + + appendSdp("a=group:BUNDLE \(bundleSsrcs.map({ "audio\($0.value)" }).joined(separator: " "))") + appendSdp("a=ice-lite") + + for ssrc in bundleSsrcs { + appendSdp("m=audio \(ssrc.isMain ? "1" : "0") RTP/SAVPF 111 126") + if ssrc.isMain { + appendSdp("c=IN IP4 0.0.0.0") + } + appendSdp("a=mid:audio\(ssrc.value)") + if ssrc.isRemoved { + appendSdp("a=inactive") + continue + } + + if ssrc.isMain { + appendSdp("a=ice-ufrag:\(transport.ufrag)") + appendSdp("a=ice-pwd:\(transport.pwd)") + + for fingerprint in transport.fingerprints { + appendSdp("a=fingerprint:\(fingerprint.hashType) \(fingerprint.fingerprint)") + appendSdp("a=setup:\(fingerprint.setup)") + } + + for candidate in transport.candidates { + var candidateString = "a=candidate:" + candidateString.append("\(candidate.foundation) ") + candidateString.append("\(candidate.component) ") + var protocolValue = candidate.protocol + if protocolValue == "ssltcp" { + protocolValue = "tcp" + } + candidateString.append("\(protocolValue) ") + candidateString.append("\(candidate.priority) ") + + var ip = candidate.ip + if ip.hasPrefix("192.") { + ip = bridgeHost + } + candidateString.append("\(ip) ") + candidateString.append("\(candidate.port) ") + + candidateString.append("typ \(candidate.type) ") + + switch candidate.type { + case "srflx", "prflx", "relay": + if let relAddr = candidate.relAddr, let relPort = candidate.relPort { + candidateString.append("raddr \(relAddr) rport \(relPort) ") + } + break + default: + break + } + + if protocolValue == "tcp" { + guard let tcpType = candidate.tcpType else { + continue + } + candidateString.append("tcptype \(tcpType) ") + } + + candidateString.append("generation \(candidate.generation)") + + appendSdp(candidateString) + } + } + + appendSdp("a=rtpmap:111 opus/48000/2") + //appendSdp("a=rtpmap:103 ISAC/16000") + //appendSdp("a=rtpmap:104 ISAC/32000") + appendSdp("a=rtpmap:126 telephone-event/8000") + appendSdp("a=fmtp:111 minptime=10; useinbandfec=1; usedtx=1") + appendSdp("a=rtcp:1 IN IP4 0.0.0.0") + appendSdp("a=rtcp-mux") + appendSdp("a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level") + appendSdp("a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") + appendSdp("a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02") + appendSdp("a=rtcp-fb:111 transport-cc") + //appendSdp("a=rtcp-fb:111 ccm fir") + //appendSdp("a=rtcp-fb:111 nack") + + if ssrc.isMain { + appendSdp("a=sendrecv") + } else { + appendSdp("a=sendonly") + appendSdp("a=bundle-only") + } + + appendSdp("a=ssrc-group:FID \(ssrc.value)") + appendSdp("a=ssrc:\(ssrc.value) cname:stream\(ssrc.value)") + appendSdp("a=ssrc:\(ssrc.value) msid:stream\(ssrc.value) audio\(ssrc.value)") + appendSdp("a=ssrc:\(ssrc.value) mslabel:audio\(ssrc.value)") + appendSdp("a=ssrc:\(ssrc.value) label:audio\(ssrc.value)") + } + + appendSdp("") + + return sdp + } + + var ssrcList: [Ssrc] = [] + var maybeMainSsrcId: Int? + for content in self.contents { + for channel in content.channels { + if channel.endpoint == bundleId { + precondition(channel.sources.count == 1) + ssrcList.append(contentsOf: channel.sources.map { ssrc in + return Ssrc( + isMain: true, + value: ssrc, + streamId: "stream0", + isRemoved: false + ) + }) + maybeMainSsrcId = channel.sources[0] + } else { + precondition(channel.ssrcs.count <= 1) + ssrcList.append(contentsOf: channel.ssrcs.map { ssrc in + return Ssrc( + isMain: false, + value: ssrc, + streamId: "stream\(ssrc)", + isRemoved: false + ) + }) + } + } + } + + guard let mainSsrcId = maybeMainSsrcId else { + preconditionFailure() + } + + var bundleSsrcs: [Ssrc] = [] + if let currentState = currentState { + for item in currentState.items { + let isRemoved = !ssrcList.contains(where: { $0.value == item.ssrc }) + bundleSsrcs.append(Ssrc( + isMain: item.ssrc == mainSsrcId, + value: item.ssrc, + streamId: item.ssrc == mainSsrcId ? "audio0" : "stream\(item.ssrc)", + isRemoved: isRemoved + )) + } + } + + for ssrc in ssrcList { + if bundleSsrcs.contains(where: { $0.value == ssrc.value }) { + continue + } + bundleSsrcs.append(ssrc) + } + + var sdpList: [String] = [] + + sdpList.append(createSdp(sessionId: sessionId, bundleSsrcs: bundleSsrcs, isPartial: false)) + + /*if currentState == nil { + sdpList.append(createSdp(sessionId: sessionId, bundleSsrcs: bundleSsrcs, isPartial: false)) + } else { + for ssrc in bundleSsrcs { + if ssrc.isMain { + continue + } + sdpList.append(createSdp(sessionId: sessionId, bundleSsrcs: [ssrc], isPartial: true)) + } + }*/ + + return RemoteOffer( + sdpList: sdpList, + isPartial: false, + state: RemoteOffer.State( + items: bundleSsrcs.map { ssrc in + RemoteOffer.State.Item( + ssrc: ssrc.value, + isRemoved: ssrc.isRemoved + ) + } + ) + ) + } + + mutating func updateLocalChannelFromSdpAnswer(bundleId: String, sdpAnswer: String) { + var maybeAudioChannel: ConferenceDescription.Content.Channel? + for content in self.contents { + for channel in content.channels { + if channel.endpoint == bundleId { + maybeAudioChannel = channel + break + } + } + } + + guard var audioChannel = maybeAudioChannel else { + assert(false) + return + } + + let lines = sdpAnswer.components(separatedBy: "\n") + func getLines(prefix: String) -> [String] { + var result: [String] = [] + for line in lines { + if line.hasPrefix(prefix) { + var cleanLine = String(line[line.index(line.startIndex, offsetBy: prefix.count)...]) + if cleanLine.hasSuffix("\r") { + cleanLine.removeLast() + } + result.append(cleanLine) + } + } + return result + } + + var audioSources: [Int] = [] + for line in getLines(prefix: "a=ssrc:") { + let scanner = Scanner(string: line) + if #available(iOS 13.0, *) { + if let ssrc = scanner.scanInt() { + if !audioSources.contains(ssrc) { + audioSources.append(ssrc) + } + } + } + } + + audioChannel.sources = audioSources + /*audioChannel.ssrcGroups = [ConferenceDescription.Content.Channel.SsrcGroup( + sources: audioSources, + semantics: "SIM" + )]*/ + + audioChannel.payloadTypes = [ + ConferenceDescription.Content.Channel.PayloadType( + id: 111, + name: "opus", + clockrate: 48000, + channels: 2, + parameters: [ + "fmtp": [ + "minptime=10;useinbandfec=1" + ] as [Any], + "rtcp-fbs": [[ + "type": "transport-cc" + ] as [String: Any]] as [Any] + ] + ), + /*ConferenceDescription.Content.Channel.PayloadType( + id: 103, + name: "ISAC", + clockrate: 16000, + channels: 1 + ), + ConferenceDescription.Content.Channel.PayloadType( + id: 104, + name: "ISAC", + clockrate: 32000, + channels: 1 + ),*/ + ConferenceDescription.Content.Channel.PayloadType( + id: 126, + name: "telephone-event", + clockrate: 8000, + channels: 1 + ) + ] + + audioChannel.rtpHdrExts = [ + ConferenceDescription.Content.Channel.RtpHdrExt( + id: 1, + uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level" + ), + ConferenceDescription.Content.Channel.RtpHdrExt( + id: 3, + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" + ), + ConferenceDescription.Content.Channel.RtpHdrExt( + id: 5, + uri: "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02" + ), + ] + + guard let ufrag = getLines(prefix: "a=ice-ufrag:").first else { + assert(false) + return + } + guard let pwd = getLines(prefix: "a=ice-pwd:").first else { + assert(false) + return + } + + var fingerprints: [ConferenceDescription.Transport.Fingerprint] = [] + for line in getLines(prefix: "a=fingerprint:") { + let components = line.components(separatedBy: " ") + if components.count != 2 { + continue + } + fingerprints.append(ConferenceDescription.Transport.Fingerprint( + fingerprint: components[1], + setup: "active", + hashType: components[0] + )) + } + + outerContents: for i in 0 ..< self.contents.count { + for j in 0 ..< self.contents[i].channels.count { + if self.contents[i].channels[j].endpoint == bundleId { + self.contents[i].channels[j] = audioChannel + break outerContents + } + } + } + + var candidates: [ConferenceDescription.Transport.Candidate] = [] + /*for line in getLines(prefix: "a=candidate:") { + let scanner = Scanner(string: line) + if #available(iOS 13.0, *) { + candidates.append(ConferenceDescription.Transport.Candidate( + id: "", + generation: 0, + component: "", + protocol: "", + tcpType: nil, + ip: "", + port: 0, + foundation: "", + priority: 0, + type: "", + network: 0, + relAddr: nil, + relPort: nil + )) + } + }*/ + + let transport = ConferenceDescription.Transport( + candidates: candidates, + fingerprints: fingerprints, + ufrag: ufrag, + pwd: pwd + ) + + var found = false + for i in 0 ..< self.channelBundles.count { + if self.channelBundles[i].id == bundleId { + self.channelBundles[i].transport = transport + found = true + break + } + } + if !found { + self.channelBundles.append(ConferenceDescription.ChannelBundle( + id: bundleId, + transport: transport + )) + } + } +} + +private enum HttpError { + case generic + case network + case server(String) +} + +private enum HttpMethod { + case get + case post([String: Any]) + case patch([String: Any]) +} + +private func httpJsonRequest(url: String, method: HttpMethod, resultType: T.Type) -> Signal { + return Signal { subscriber in + guard let url = URL(string: url) else { + subscriber.putError(.generic) + return EmptyDisposable + } + let completed = Atomic(value: false) + + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 1000.0) + + switch method { + case .get: + break + case let .post(data): + guard let body = try? JSONSerialization.data(withJSONObject: data, options: []) else { + subscriber.putError(.generic) + return EmptyDisposable + } + + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = body + request.httpMethod = "POST" + case let .patch(data): + guard let body = try? JSONSerialization.data(withJSONObject: data, options: []) else { + subscriber.putError(.generic) + return EmptyDisposable + } + + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = body + request.httpMethod = "PATCH" + + //print("PATCH: \(String(data: body, encoding: .utf8)!)") + } + + let task = URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in + if let error = error { + print("\(error)") + subscriber.putError(.server("\(error)")) + return + } + + let _ = completed.swap(true) + if let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? T { + subscriber.putNext(json) + subscriber.putCompletion() + } else { + subscriber.putError(.network) + } + }) + task.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + task.cancel() + } + } + } +} + +public final class GroupCallContext { + private final class Impl { + private let queue: Queue + private let context: GroupCallThreadLocalContext + private let disposable = MetaDisposable() + + private let colibriHost: String + private let sessionId: UInt32 + + private var audioSessionDisposable: Disposable? + private let pollDisposable = MetaDisposable() + + private var conferenceId: String? + private var localBundleId: String? + private var localTransport: ConferenceDescription.Transport? + + let memberCount = ValuePromise(0, ignoreRepeated: true) + + private var isMutedValue: Bool = false + let isMuted = ValuePromise(false, ignoreRepeated: true) + + init(queue: Queue, audioSessionActive: Signal) { + self.queue = queue + + self.sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) + self.colibriHost = "51.11.141.27" + //self.colibriHost = "192.168.93.24" + //self.colibriHost = "51.104.206.109" + + var relaySdpAnswerImpl: ((String) -> Void)? + + self.context = GroupCallThreadLocalContext(queue: ContextQueueImpl(queue: queue), relaySdpAnswer: { sdpAnswer in + queue.async { + relaySdpAnswerImpl?(sdpAnswer) + } + }) + + relaySdpAnswerImpl = { [weak self] sdpAnswer in + guard let strongSelf = self else { + return + } + strongSelf.relaySdpAnswer(sdpAnswer: sdpAnswer) + } + + self.audioSessionDisposable = (audioSessionActive + |> filter { $0 } + |> take(1) + |> deliverOn(queue)).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.requestConference() + }) + } + + deinit { + self.disposable.dispose() + self.audioSessionDisposable?.dispose() + self.pollDisposable.dispose() + } + + func requestConference() { + self.disposable.set((httpJsonRequest(url: "http://\(self.colibriHost):8080/colibri/conferences/", method: .get, resultType: [Any].self) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + + if let conferenceJson = result.first as? [String: Any] { + if let conferenceId = ConferenceDescription(json: conferenceJson)?.id { + strongSelf.disposable.set((httpJsonRequest(url: "http://\(strongSelf.colibriHost):8080/colibri/conferences/\(conferenceId)", method: .get, resultType: [String: Any].self) + |> deliverOn(strongSelf.queue)).start(next: { result in + guard let strongSelf = self else { + return + } + if let conference = ConferenceDescription(json: result) { + strongSelf.allocateChannels(conference: conference) + } + })) + } + } else { + strongSelf.disposable.set((httpJsonRequest(url: "http://\(strongSelf.colibriHost):8080/colibri/conferences/", method: .post([:]), resultType: [String: Any].self) + |> deliverOn(strongSelf.queue)).start(next: { result in + guard let strongSelf = self else { + return + } + if let conference = ConferenceDescription(json: result) { + strongSelf.allocateChannels(conference: conference) + } + })) + } + })) + } + + private var currentOfferState: RemoteOffer.State? + + func allocateChannels(conference: ConferenceDescription) { + let bundleId = UUID().uuidString + + var conference = conference + let audioChannel = ConferenceDescription.Content.Channel( + id: nil, + endpoint: bundleId, + channelBundleId: bundleId, + sources: [], + ssrcs: [], + rtpLevelRelayType: "translator", + expire: 10, + initiator: true, + direction: "sendrecv", + ssrcGroups: [], + payloadTypes: [], + rtpHdrExts: [] + ) + + var foundContent = false + for i in 0 ..< conference.contents.count { + if conference.contents[i].name == "audio" { + for j in 0 ..< conference.contents[i].channels.count { + let channel = conference.contents[i].channels[j] + conference.contents[i].channels[j] = ConferenceDescription.Content.Channel( + id: channel.id, + endpoint: channel.endpoint, + channelBundleId: channel.channelBundleId, + sources: channel.sources, + ssrcs: channel.ssrcs, + rtpLevelRelayType: channel.rtpLevelRelayType, + expire: channel.expire, + initiator: channel.initiator, + direction: channel.direction, + ssrcGroups: [], + payloadTypes: [], + rtpHdrExts: [] + ) + } + conference.contents[i].channels.append(audioChannel) + foundContent = true + break + } + } + if !foundContent { + conference.contents.append(ConferenceDescription.Content( + name: "audio", + channels: [audioChannel] + )) + } + conference.channelBundles.append(ConferenceDescription.ChannelBundle( + id: bundleId, + transport: ConferenceDescription.Transport( + candidates: [], + fingerprints: [], + ufrag: "", + pwd: "" + ) + )) + + var payload = conference.outgoingColibriDescription() + if var contents = payload["contents"] as? [[String: Any]] { + for contentIndex in 0 ..< contents.count { + if var channels = contents[contentIndex]["channels"] as? [Any] { + for i in (0 ..< channels.count).reversed() { + if var channel = channels[i] as? [String: Any] { + if channel["endpoint"] as? String != bundleId { + channel = ["id": channel["id"]!] + channels[i] = channel + channels.remove(at: i) + } + } + } + contents[contentIndex]["channels"] = channels + } + } + payload["contents"] = contents + } + + self.disposable.set((httpJsonRequest(url: "http://\(self.colibriHost):8080/colibri/conferences/\(conference.id)", method: .patch(payload), resultType: [String: Any].self) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + + guard let conference = ConferenceDescription(json: result) else { + return + } + + var maybeTransport: ConferenceDescription.Transport? + for channelBundle in conference.channelBundles { + if channelBundle.id == bundleId { + maybeTransport = channelBundle.transport + break + } + } + + guard let transport = maybeTransport else { + assert(false) + return + } + + strongSelf.conferenceId = conference.id + strongSelf.localBundleId = bundleId + strongSelf.localTransport = transport + + //strongSelf.context.emitOffer() + + guard let offer = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: bundleId, bridgeHost: strongSelf.colibriHost, transport: transport, currentState: strongSelf.currentOfferState) else { + return + } + strongSelf.currentOfferState = offer.state + + strongSelf.memberCount.set(offer.state.items.filter({ !$0.isRemoved }).count) + + for sdp in offer.sdpList { + strongSelf.context.setOfferSdp(sdp, isPartial: offer.isPartial) + } + })) + } + + private func relaySdpAnswer(sdpAnswer: String) { + guard let conferenceId = self.conferenceId, let localBundleId = self.localBundleId else { + return + } + + self.disposable.set((httpJsonRequest(url: "http://\(self.colibriHost):8080/colibri/conferences/\(conferenceId)", method: .get, resultType: [String: Any].self) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + + guard var conference = ConferenceDescription(json: result) else { + return + } + + conference.updateLocalChannelFromSdpAnswer(bundleId: localBundleId, sdpAnswer: sdpAnswer) + + var payload = conference.outgoingColibriDescription() + if var contents = payload["contents"] as? [[String: Any]] { + for contentIndex in 0 ..< contents.count { + if var channels = contents[contentIndex]["channels"] as? [Any] { + for i in (0 ..< channels.count).reversed() { + if var channel = channels[i] as? [String: Any] { + if channel["endpoint"] as? String != localBundleId { + channel = ["id": channel["id"]!] + channels[i] = channel + channels.remove(at: i) + } + } + } + contents[contentIndex]["channels"] = channels + } + } + payload["contents"] = contents + } + + strongSelf.disposable.set((httpJsonRequest(url: "http://\(strongSelf.colibriHost):8080/colibri/conferences/\(conference.id)", method: .patch(payload), resultType: [String: Any].self) + |> deliverOn(strongSelf.queue)).start(next: { result in + guard let strongSelf = self else { + return + } + + guard let conference = ConferenceDescription(json: result) else { + return + } + + if conference.id == strongSelf.conferenceId { + strongSelf.pollOnceDelayed() + } + })) + })) + } + + private func pollOnceDelayed() { + guard let conferenceId = self.conferenceId, let localBundleId = self.localBundleId, let localTransport = self.localTransport else { + return + } + self.pollDisposable.set((httpJsonRequest(url: "http://\(self.colibriHost):8080/colibri/conferences/\(conferenceId)", method: .get, resultType: [String: Any].self) + |> delay(1.0, queue: self.queue) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + + guard let conference = ConferenceDescription(json: result) else { + return + } + + guard conference.id == strongSelf.conferenceId else { + return + } + + if let offer = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: localBundleId, bridgeHost: strongSelf.colibriHost, transport: localTransport, currentState: strongSelf.currentOfferState) { + strongSelf.currentOfferState = offer.state + + strongSelf.memberCount.set(offer.state.items.filter({ !$0.isRemoved }).count) + + for sdp in offer.sdpList { + strongSelf.context.setOfferSdp(sdp, isPartial: offer.isPartial) + } + } + + strongSelf.pollOnceDelayed() + })) + } + + func toggleIsMuted() { + self.isMutedValue = !self.isMutedValue + self.isMuted.set(self.isMutedValue) + self.context.setIsMuted(self.isMutedValue) + } + } + + private let queue = Queue() + private let impl: QueueLocalObject + + public init(audioSessionActive: Signal) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, audioSessionActive: audioSessionActive) + }) + } + + public var memberCount: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.memberCount.get().start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public var isMuted: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.isMuted.get().start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public func toggleIsMuted() { + self.impl.with { impl in + impl.toggleIsMuted() + } + } +} diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h new file mode 100644 index 0000000000..ba75ba664a --- /dev/null +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h @@ -0,0 +1,18 @@ +#ifndef GroupCallThreadLocalContext_h +#define GroupCallThreadLocalContext_h + +#import + +#import + +@interface GroupCallThreadLocalContext : NSObject + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer; + +- (void)emitOffer; +- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial; +- (void)setIsMuted:(bool)isMuted; + +@end + +#endif diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h similarity index 100% rename from submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h rename to submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h diff --git a/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm new file mode 100644 index 0000000000..0cf157d985 --- /dev/null +++ b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm @@ -0,0 +1,57 @@ + +#import + +#import "group/GroupInstanceImpl.h" + +@interface GroupCallThreadLocalContext () { + id _queue; + + std::unique_ptr _instance; +} + +@end + +@implementation GroupCallThreadLocalContext + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer { + self = [super init]; + if (self != nil) { + _queue = queue; + + tgcalls::GroupInstanceDescriptor descriptor; + __weak GroupCallThreadLocalContext *weakSelf = self; + descriptor.sdpAnswerEmitted = [weakSelf, queue, relaySdpAnswer](std::string const &sdpAnswer) { + NSString *string = [NSString stringWithUTF8String:sdpAnswer.c_str()]; + [queue dispatch:^{ + __strong GroupCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + relaySdpAnswer(string); + }]; + }; + + _instance.reset(new tgcalls::GroupInstanceImpl(std::move(descriptor))); + } + return self; +} + +- (void)emitOffer { + if (_instance) { + _instance->emitOffer(); + } +} + +- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial { + if (_instance) { + _instance->setOfferSdp([offerSdp UTF8String], isPartial); + } +} + +- (void)setIsMuted:(bool)isMuted { + if (_instance) { + _instance->setIsMuted(isMuted); + } +} + +@end diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index a5ce0fc9e2..9a27674497 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1,7 +1,7 @@ #ifndef WEBRTC_IOS #import "OngoingCallThreadLocalContext.h" #else -#import +#import #endif diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 64f96a1b4f..4f7501d281 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 64f96a1b4fcfb8afdb0fb7749082cb42cdad7901 +Subproject commit 4f7501d281b851e6302b2a2d7298c733eee82414 diff --git a/third-party/webrtc/BUILD b/third-party/webrtc/BUILD index 972b497880..b6b7def5cb 100644 --- a/third-party/webrtc/BUILD +++ b/third-party/webrtc/BUILD @@ -1,4 +1,4 @@ -use_gn_build = True +use_gn_build = False webrtc_libs = [ "libwebrtc.a", diff --git a/third-party/webrtc/webrtc-ios b/third-party/webrtc/webrtc-ios index 11255bcfff..782743c793 160000 --- a/third-party/webrtc/webrtc-ios +++ b/third-party/webrtc/webrtc-ios @@ -1 +1 @@ -Subproject commit 11255bcfff3180210a012f368e2d2bcd169b6877 +Subproject commit 782743c7931d09c1d2e4a0cf6cd349ee45452f1d