From beb474160728ba6e0ead3ab7c379cea9283e1c97 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Oct 2020 10:36:03 +0100 Subject: [PATCH 01/12] WIP --- .../Sources/PeerInfo/PeerInfoScreen.swift | 8 + .../Sources/GroupCallContext.swift | 570 ++++++++++++++++++ .../GroupCallThreadLocalContext.h | 16 + .../OngoingCallThreadLocalContext.h | 0 .../Sources/GroupCallThreadLocalContext.mm | 45 ++ .../Sources/OngoingCallThreadLocalContext.mm | 2 +- submodules/TgVoipWebrtc/tgcalls | 2 +- 7 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 submodules/TelegramVoip/Sources/GroupCallContext.swift create mode 100644 submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h rename submodules/TgVoipWebrtc/PublicHeaders/{TgVoip => TgVoipWebrtc}/OngoingCallThreadLocalContext.h (100%) create mode 100644 submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 06a3944c2c..1a04e5bec9 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 } @@ -3153,7 +3154,14 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.present(shareController, in: .window(.root)) } + private var groupCall: GroupCallContext? + private func requestCall(isVideo: Bool) { + #if DEBUG + self.groupCall = GroupCallContext() + return; + #endif + guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { return } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift new file mode 100644 index 0000000000..f549da2fb9 --- /dev/null +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -0,0 +1,570 @@ +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() + } +} + +/* + v=0 + o=- 3432551037272164134 2 IN IP4 127.0.0.1 + s=- + t=0 0 + a=group:BUNDLE audio + a=msid-semantic: WMS stream0 + m=audio 9 RTP/SAVPF 103 104 126 + c=IN IP4 0.0.0.0 + a=rtcp:9 IN IP4 0.0.0.0 + a=ice-ufrag:XTZl + a=ice-pwd:GS+K9fcajkZ96gy5hCIyx1BV + a=ice-options:trickle + a=fingerprint:sha-256 88:A3:3E:2C:E3:3C:DF:E8:31:1B:59:AA:73:60:D8:EF:E7:FE:0D:F5:B8:F1:79:26:58:A3:D2:93:D9:8C:49:29 + a=setup:active + a=mid:audio + a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level + a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time + a=sendrecv + a=msid:stream0 audio0 + a=rtcp-mux + a=rtpmap:103 ISAC/16000 + a=rtpmap:104 ISAC/32000 + a=rtpmap:126 telephone-event/8000 + a=ssrc:666769703 cname:g5ORSLYV5oOfoEBX + a=ssrc:666769703 msid:stream0 audio0 + a=ssrc:666769703 mslabel:stream0 + a=ssrc:666769703 label:audio0 + */ + +private func convertSDPToColibri(conferenceId: String, audioChannelId: String, audioChannelExpire: Int, audioChannelEndpoint: String, audioChannelDirection: String, string: String) -> [String: Any]? { + let lines = string.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 result: [String: Any] = [:] + + result["id"] = conferenceId + + var contents: [Any] = [] + + var audio: [String: Any] = [:] + audio["name"] = "audio" + + var audioChannel: [String: Any] = [:] + audioChannel["id"] = audioChannelId + audioChannel["expire"] = audioChannelExpire + audioChannel["endpoint"] = audioChannelEndpoint + audioChannel["direction"] = audioChannelDirection + audioChannel["channel-bundle-id"] = audioChannelEndpoint + 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["ssrc-groups"] = [ + "semantics": "SIM", + "sources": audioSources + ] as [String: Any] + audioChannel["rtp-level-relay-type"] = "translator" + + audioChannel["payload-types"] = [ + [ + "id": 111, + "name": "opus", + "clockrate": 48000, + "channels": 2, + "parameters": [ + "fmtp": [ + "minptime=10;useinbandfec=1" + ] as [Any] + ] as [String: Any] + ] as [String: Any], + [ + "id": 103, + "name": "ISAC", + "clockrate": 16000, + "channels": 1 + ] as [String: Any], + [ + "id": 104, + "name": "ISAC", + "clockrate": 32000, + "channels": 1 + ] as [String: Any], + [ + "id": 126, + "name": "telephone-event", + "clockrate": 8000, + "channels": 1 + ] as [String: Any], + ] as Any + + audioChannel["rtp-hdrexts"] = [ + [ + "id": 1, + "uri": "urn:ietf:params:rtp-hdrext:ssrc-audio-level" + ] as [String: Any], + [ + "id": 3, + "uri": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" + ] as [String: Any] + ] as [Any] + + guard let ufrag = getLines(prefix: "a=ice-ufrag:").first else { + return nil + } + guard let pwd = getLines(prefix: "a=ice-pwd:").first else { + return nil + } + + var fingerprints: [[String: Any]] = [] + for line in getLines(prefix: "a=fingerprint:") { + let components = line.components(separatedBy: " ") + if components.count != 2 { + continue + } + fingerprints.append([ + "hash": components[0], + "fingerprint": components[1], + "setup": "active" + ]) + } + + /*audioChannel["transport"] = [ + "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", + "rtcp-mux": true, + "pwd": pwd, + "ufrag": ufrag, + "fingerprints": fingerprints, + "candidates": [ + ] as [Any] + ] as [String: Any]*/ + + + audio["channels"] = [audioChannel] + contents.append(audio) + + result["contents"] = contents + + result["channel-bundles"] = [ + [ + "id": audioChannelEndpoint, + "transport": [ + "candidates": [ + + ] as [Any], + "fingerprints": fingerprints, + "pwd": pwd, + "ufrag": ufrag, + "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", + "rtcp-mux": true + ] as [String: Any] + ] as [String: Any] + ] as [Any] + + return result +} + +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) -> Signal<[String: Any], HttpError> { + 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? [String: Any] { + 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 { + enum State { + case empty + case requestingConference + case allocatingChannels(conferenceId: String) + } + + private let queue: Queue + private let context: GroupCallThreadLocalContext + private let disposable = MetaDisposable() + + private var conferenceId: String? + private var audioChannelId: String? + private var audioChannelExpire: Int? + private var audioChannelEndpoint: String? + private var audioChannelDirection: String? + + init(queue: Queue) { + self.queue = queue + + 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.requestConference() + } + + deinit { + self.disposable.dispose() + } + + func requestConference() { + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/", method: .post([:])) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + guard let conferenceId = result["id"] as? String else { + return + } + strongSelf.allocateChannels(conferenceId: conferenceId) + })) + } + + func allocateChannels(conferenceId: String) { + let bundleId = UUID().uuidString + + let audioChannelEndpoint = bundleId + let audioChannelExpire = 30 + let audioChannelDirection = "sendrecv" + + let payload: [String: Any] = [ + "id": conferenceId, + "contents": [ + [ + "name": "audio", + "channels": [ + [ + "expire": audioChannelExpire, + "initiator": true, + "endpoint": audioChannelEndpoint, + "direction": audioChannelDirection, + "channel-bundle-id": audioChannelEndpoint, + "rtp-level-relay-type": "mixer" + ] as [String: Any] + ] as [Any] + ] as [String: Any] + ] as [Any], + "channel-bundles": [ + [ + "id": "\(bundleId)", + "transport": [ + "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", + "rtcp-mux": true + ] as [String: Any] + ] as [String: Any] + ] as [Any] + ] + + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId)", method: .patch(payload)) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + guard let channelBundles = result["channel-bundles"] as? [Any] else { + return + } + guard let channelBundle = channelBundles.first as? [String: Any] else { + return + } + guard let transport = channelBundle["transport"] as? [String: Any] else { + return + } + guard let contents = result["contents"] as? [Any] else { + return + } + + var audioChannelId: String? + for item in contents { + guard let item = item as? [String: Any] else { + continue + } + guard let channels = item["channels"] as? [Any] else { + continue + } + for channel in channels { + if let channel = channel as? [String: Any] { + if let id = channel["id"] as? String { + audioChannelId = id + } + } + } + } + + let uniqueId = Int(Date().timeIntervalSince1970) + + var sdp = "" + func appendSdp(_ string: String) { + if !sdp.isEmpty { + sdp.append("\n") + } + sdp.append(string) + } + + appendSdp("v=0") + appendSdp("o=- \(uniqueId) 2 IN IP4 0.0.0.0") + appendSdp("s=-") + appendSdp("t=0 0") + appendSdp("a=group:BUNDLE audio") + appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") + appendSdp("c=IN IP4 0.0.0.0") + 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") + appendSdp("a=rtcp:1 IN IP4 0.0.0.0") + 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=setup:actpass") + appendSdp("a=mid:audio") + appendSdp("a=sendrecv") + + guard let ufrag = transport["ufrag"] as? String else { + return + } + guard let pwd = transport["pwd"] as? String else { + return + } + + appendSdp("a=ice-ufrag:\(ufrag)") + appendSdp("a=ice-pwd:\(pwd)") + + if let fingerprints = transport["fingerprints"] as? [Any] { + for fingerprint in fingerprints { + if let fingerprint = fingerprint as? [String: Any] { + guard let fingerprintValue = fingerprint["fingerprint"] as? String else { + continue + } + guard let hashMethod = fingerprint["hash"] as? String else { + continue + } + appendSdp("a=fingerprint:\(hashMethod) \(fingerprintValue)") + } + } + } + + if let candidates = transport["candidates"] as? [Any] { + for candidate in candidates { + if let candidate = candidate as? [String: Any] { + var candidateString = "a=candidate:" + guard let foundation = candidate["foundation"] as? String else { + continue + } + candidateString.append("\(foundation) ") + guard let component = candidate["component"] as? String else { + continue + } + candidateString.append("\(component) ") + guard var protocolValue = candidate["protocol"] as? String else { + continue + } + if protocolValue == "ssltcp" { + protocolValue = "tcp" + } + candidateString.append("\(protocolValue) ") + + guard let priority = candidate["priority"] as? String else { + continue + } + candidateString.append("\(priority) ") + + guard let ip = candidate["ip"] as? String else { + continue + } + //candidateString.append("\(ip) ") + candidateString.append("127.0.0.1 ") + + guard let port = candidate["port"] as? String else { + continue + } + candidateString.append("\(port) ") + + guard let type = candidate["type"] as? String else { + continue + } + candidateString.append("typ \(type) ") + + switch type { + case "srflx", "prflx", "relay": + if let relAddr = candidate["rel-addr"] as? String, let relPort = candidate["rel-port"] as? String { + candidateString.append("raddr \(relAddr) rport \(relPort) ") + } + break + default: + break + } + + if protocolValue == "tcp" { + guard let tcpType = candidate["tcptype"] as? String else { + continue + } + candidateString.append("tcptype \(tcpType) ") + } + + candidateString.append("generation ") + if let generation = candidate["generation"] as? String { + candidateString.append(generation) + } else { + candidateString.append("0") + } + + appendSdp(candidateString) + } + } + } + + appendSdp("a=rtcp-mux") + appendSdp("") + + strongSelf.conferenceId = conferenceId + strongSelf.audioChannelId = audioChannelId + strongSelf.audioChannelExpire = audioChannelExpire + strongSelf.audioChannelEndpoint = audioChannelEndpoint + strongSelf.audioChannelDirection = audioChannelDirection + + strongSelf.context.setOfferSdp(sdp) + })) + } + + private func relaySdpAnswer(sdpAnswer: String) { + guard let payload = convertSDPToColibri( + conferenceId: conferenceId!, + audioChannelId: audioChannelId!, + audioChannelExpire: audioChannelExpire!, + audioChannelEndpoint: audioChannelEndpoint!, + audioChannelDirection: audioChannelDirection!, + string: sdpAnswer + ) else { + return + } + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId!)", method: .patch(payload)) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + })) + } + } + + private let queue = Queue() + private let impl: QueueLocalObject + + public init() { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue) + }) + } +} diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h new file mode 100644 index 0000000000..3e67424272 --- /dev/null +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h @@ -0,0 +1,16 @@ +#ifndef GroupCallThreadLocalContext_h +#define GroupCallThreadLocalContext_h + +#import + +#import + +@interface GroupCallThreadLocalContext : NSObject + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer; + +- (void)setOfferSdp:(NSString * _Nonnull)offerSdp; + +@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..fca53a91eb --- /dev/null +++ b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm @@ -0,0 +1,45 @@ + +#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)setOfferSdp:(NSString * _Nonnull)offerSdp { + if (_instance) { + _instance->setOfferSdp([offerSdp UTF8String]); + } +} + +@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..4949dc52e3 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 64f96a1b4fcfb8afdb0fb7749082cb42cdad7901 +Subproject commit 4949dc52e358fe623229846fa78f72b7a6b55d16 From 9658c703cfc8dfac28337918cbbcfbe2304527bd Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Oct 2020 11:30:25 +0100 Subject: [PATCH 02/12] Fix format --- .../Sources/GroupCallContext.swift | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index f549da2fb9..3a2e915dc9 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -100,10 +100,11 @@ private func convertSDPToColibri(conferenceId: String, audioChannelId: String, a } } audioChannel["sources"] = audioSources - audioChannel["ssrc-groups"] = [ + let ssrcGroup = [ "semantics": "SIM", "sources": audioSources ] as [String: Any] + audioChannel["ssrc-groups"] = [ssrcGroup] audioChannel["rtp-level-relay-type"] = "translator" audioChannel["payload-types"] = [ @@ -216,7 +217,7 @@ private enum HttpMethod { case patch([String: Any]) } -private func httpJsonRequest(url: String, method: HttpMethod) -> Signal<[String: Any], HttpError> { +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) @@ -259,7 +260,7 @@ private func httpJsonRequest(url: String, method: HttpMethod) -> Signal<[String: } let _ = completed.swap(true) - if let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + if let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? T { subscriber.putNext(json) subscriber.putCompletion() } else { @@ -320,15 +321,26 @@ public final class GroupCallContext { } func requestConference() { - self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/", method: .post([:])) + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/", method: .get, resultType: [Any].self) |> deliverOn(self.queue)).start(next: { [weak self] result in guard let strongSelf = self else { return } - guard let conferenceId = result["id"] as? String else { - return + + if let conference = result.first as? [String: Any], let conferenceId = conference["id"] as? String { + strongSelf.allocateChannels(conferenceId: conferenceId) + } else { + strongSelf.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/", method: .post([:]), resultType: [String: Any].self) + |> deliverOn(strongSelf.queue)).start(next: { result in + guard let strongSelf = self else { + return + } + guard let conferenceId = result["id"] as? String else { + return + } + strongSelf.allocateChannels(conferenceId: conferenceId) + })) } - strongSelf.allocateChannels(conferenceId: conferenceId) })) } @@ -367,7 +379,7 @@ public final class GroupCallContext { ] as [Any] ] - self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId)", method: .patch(payload)) + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId)", method: .patch(payload), resultType: [String: Any].self) |> deliverOn(self.queue)).start(next: { [weak self] result in guard let strongSelf = self else { return @@ -549,7 +561,7 @@ public final class GroupCallContext { ) else { return } - self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId!)", method: .patch(payload)) + self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId!)", method: .patch(payload), resultType: [String: Any].self) |> deliverOn(self.queue)).start(next: { [weak self] result in guard let strongSelf = self else { return From 55c2465d71ca3efa0f8f188e95d329e2482d873c Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 13 Oct 2020 23:01:44 +0400 Subject: [PATCH 03/12] [WIP] --- .../Sources/GroupCallController.swift | 0 .../Sources/PeerInfo/PeerInfoScreen.swift | 16 +- .../Sources/GroupCallContext.swift | 1653 +++++++++++++---- 3 files changed, 1288 insertions(+), 381 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/GroupCallController.swift diff --git a/submodules/TelegramCallsUI/Sources/GroupCallController.swift b/submodules/TelegramCallsUI/Sources/GroupCallController.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1a04e5bec9..c0b5305558 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3154,11 +3154,25 @@ 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) { #if DEBUG - self.groupCall = GroupCallContext() + + let audioSessionActive = Promise(false) + self.groupCallDisposable.set(self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] audioSessionControl in + audioSessionControl.activate({ _ in }) + audioSessionActive.set(.single(true)) + }, deactivate: { + return Signal { subscriber in + subscriber.putCompletion() + return EmptyDisposable + } + }, availableOutputsChanged: { _, _ in + })) + + self.groupCall = GroupCallContext(audioSessionActive: audioSessionActive.get()) return; #endif diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 3a2e915dc9..3dbcfb216e 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -1,6 +1,5 @@ import Foundation import SwiftSignalKit - import TgVoipWebrtc private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc { @@ -27,182 +26,1080 @@ private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQue } } -/* - v=0 - o=- 3432551037272164134 2 IN IP4 127.0.0.1 - s=- - t=0 0 - a=group:BUNDLE audio - a=msid-semantic: WMS stream0 - m=audio 9 RTP/SAVPF 103 104 126 - c=IN IP4 0.0.0.0 - a=rtcp:9 IN IP4 0.0.0.0 - a=ice-ufrag:XTZl - a=ice-pwd:GS+K9fcajkZ96gy5hCIyx1BV - a=ice-options:trickle - a=fingerprint:sha-256 88:A3:3E:2C:E3:3C:DF:E8:31:1B:59:AA:73:60:D8:EF:E7:FE:0D:F5:B8:F1:79:26:58:A3:D2:93:D9:8C:49:29 - a=setup:active - a=mid:audio - a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level - a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - a=sendrecv - a=msid:stream0 audio0 - a=rtcp-mux - a=rtpmap:103 ISAC/16000 - a=rtpmap:104 ISAC/32000 - a=rtpmap:126 telephone-event/8000 - a=ssrc:666769703 cname:g5ORSLYV5oOfoEBX - a=ssrc:666769703 msid:stream0 audio0 - a=ssrc:666769703 mslabel:stream0 - a=ssrc:666769703 label:audio0 - */ - -private func convertSDPToColibri(conferenceId: String, audioChannelId: String, audioChannelExpire: Int, audioChannelEndpoint: String, audioChannelDirection: String, string: String) -> [String: Any]? { - let lines = string.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() +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) + } } - result.append(cleanLine) } } + 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 + 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() } + } + + 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 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 } - var result: [String: Any] = [:] - - result["id"] = conferenceId - - var contents: [Any] = [] - - var audio: [String: Any] = [:] - audio["name"] = "audio" - - var audioChannel: [String: Any] = [:] - audioChannel["id"] = audioChannelId - audioChannel["expire"] = audioChannelExpire - audioChannel["endpoint"] = audioChannelEndpoint - audioChannel["direction"] = audioChannelDirection - audioChannel["channel-bundle-id"] = audioChannelEndpoint - 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) + func offerSdp(sessionId: UInt32, bundleId: String, bridgeHost: String, transport: ConferenceDescription.Transport, currentSsrcOrder: [Int]) -> (String, [Int])? { + var otherSsrc: [(Bool, Int, String)] = [] + for content in self.contents { + for channel in content.channels { + if channel.endpoint == bundleId { + otherSsrc.append(contentsOf: channel.sources.map { ssrc in + return (true, ssrc, "stream0") + }) + } else { + otherSsrc.append(contentsOf: channel.ssrcs.map { ssrc in + return (false, ssrc, channel.channelBundleId) + }) } } } - } - audioChannel["sources"] = audioSources - let ssrcGroup = [ - "semantics": "SIM", - "sources": audioSources - ] as [String: Any] - audioChannel["ssrc-groups"] = [ssrcGroup] - audioChannel["rtp-level-relay-type"] = "translator" - - audioChannel["payload-types"] = [ - [ - "id": 111, - "name": "opus", - "clockrate": 48000, - "channels": 2, - "parameters": [ - "fmtp": [ - "minptime=10;useinbandfec=1" - ] as [Any] - ] as [String: Any] - ] as [String: Any], - [ - "id": 103, - "name": "ISAC", - "clockrate": 16000, - "channels": 1 - ] as [String: Any], - [ - "id": 104, - "name": "ISAC", - "clockrate": 32000, - "channels": 1 - ] as [String: Any], - [ - "id": 126, - "name": "telephone-event", - "clockrate": 8000, - "channels": 1 - ] as [String: Any], - ] as Any - - audioChannel["rtp-hdrexts"] = [ - [ - "id": 1, - "uri": "urn:ietf:params:rtp-hdrext:ssrc-audio-level" - ] as [String: Any], - [ - "id": 3, - "uri": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" - ] as [String: Any] - ] as [Any] - - guard let ufrag = getLines(prefix: "a=ice-ufrag:").first else { - return nil - } - guard let pwd = getLines(prefix: "a=ice-pwd:").first else { - return nil - } - - var fingerprints: [[String: Any]] = [] - for line in getLines(prefix: "a=fingerprint:") { - let components = line.components(separatedBy: " ") - if components.count != 2 { - continue + otherSsrc.sort(by: { lhs, rhs in + /*if let previousLhsIndex = currentSsrcOrder.firstIndex(of: lhs.1), let previousRhsIndex = currentSsrcOrder.firstIndex(of: rhs.1) { + return previousLhsIndex < previousRhsIndex + } + if currentSsrcOrder.contains(lhs.1) != currentSsrcOrder.contains(rhs.1) { + return currentSsrcOrder.contains(lhs.1) + }*/ + if lhs.0 != rhs.0 { + return lhs.0 + } else { + return lhs.1 < rhs.1 + } + }) + + var sdp = "" + func appendSdp(_ string: String) { + if !sdp.isEmpty { + sdp.append("\n") + } + sdp.append(string) } - fingerprints.append([ - "hash": components[0], - "fingerprint": components[1], - "setup": "active" - ]) + + appendSdp("v=0") + appendSdp("o=- \(sessionId) 2 IN IP4 0.0.0.0") + appendSdp("s=-") + appendSdp("t=0 0") + + /*appendSdp("a=group:BUNDLE audio0") + do { + appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") + appendSdp("c=IN IP4 0.0.0.0") + 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") + appendSdp("a=rtcp:1 IN IP4 0.0.0.0") + 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=mid:audio0") + appendSdp("a=sendrecv") + 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 + 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) + } + + for ssrc in bridgeSources { + appendSdp("a=ssrc:\(ssrc) cname:cname\(ssrc)") + appendSdp("a=ssrc:\(ssrc) msid:stream0 audio0") + appendSdp("a=ssrc:\(ssrc) mslabel:stream0") + appendSdp("a=ssrc:\(ssrc) label:audio0") + } + + /*for (ssrc, streamId) in otherSsrc { + appendSdp("a=ssrc:\(ssrc) cname:cname\(ssrc)") + appendSdp("a=ssrc:\(ssrc) msid:\(streamId) audio0") + //appendSdp("a=ssrc:\(ssrc) mslabel:\(streamId)") + //appendSdp("a=ssrc:\(ssrc) label:\(streamId)") + }*/ + + appendSdp("a=rtcp-mux") + }*/ + + appendSdp("a=group:BUNDLE audio") + appendSdp("a=ice-lite") + + appendSdp("a=msid-semantic:WMS *") + + appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") + + appendSdp("c=IN IP4 0.0.0.0") + + 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 + 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") + 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=mid:audio") + appendSdp("a=sendrecv") + + for (_, ssrc, streamId) in otherSsrc { + appendSdp("a=ssrc-group:FID \(ssrc)") + appendSdp("a=ssrc:\(ssrc) cname:stream\(streamId)") + appendSdp("a=ssrc:\(ssrc) msid:stream\(streamId) audio\(streamId)") + //appendSdp("a=ssrc:\(ssrc) mslabel:stream\(streamId)") + //appendSdp("a=ssrc:\(ssrc) label:audio\(streamId)") + } + + /*for (isBridge, ssrc, streamId) in otherSsrc { + let mPort: Int + if isBridge { + mPort = 1 + } else { + mPort = 0 + } + appendSdp("m=audio \(mPort) RTP/SAVPF 111 103 104 126") + appendSdp("c=IN IP4 0.0.0.0") + + if isBridge { + appendSdp("a=sendrecv") + } else { + appendSdp("a=bundle-only") + appendSdp("a=sendonly") + } + + 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") + appendSdp("a=rtcp:1 IN IP4 0.0.0.0") + 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=mid:audio\(ssrc)") + + if isBridge { + 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 + 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=ssrc:\(ssrc) cname:stream\(streamId)") + appendSdp("a=ssrc:\(ssrc) msid:stream\(streamId) audio\(streamId)") + //appendSdp("a=ssrc:\(ssrc) mslabel:stream\(streamId)") + //appendSdp("a=ssrc:\(ssrc) label:audio\(streamId)") + + appendSdp("a=rtcp-mux") + }*/ + + appendSdp("") + + return (sdp, otherSsrc.map(\.1)) } - /*audioChannel["transport"] = [ - "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", - "rtcp-mux": true, - "pwd": pwd, - "ufrag": ufrag, - "fingerprints": fingerprints, - "candidates": [ - ] as [Any] - ] as [String: Any]*/ - - - audio["channels"] = [audioChannel] - contents.append(audio) - - result["contents"] = contents - - result["channel-bundles"] = [ - [ - "id": audioChannelEndpoint, - "transport": [ - "candidates": [ - - ] as [Any], - "fingerprints": fingerprints, - "pwd": pwd, - "ufrag": ufrag, - "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", - "rtcp-mux": true - ] as [String: Any] - ] as [String: Any] - ] as [Any] - - return result + 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 + } + + /* + v=0 + o=- 3432551037272164134 2 IN IP4 127.0.0.1 + s=- + t=0 0 + a=group:BUNDLE audio + a=msid-semantic: WMS stream0 + m=audio 9 RTP/SAVPF 103 104 126 + c=IN IP4 0.0.0.0 + a=rtcp:9 IN IP4 0.0.0.0 + a=ice-ufrag:XTZl + a=ice-pwd:GS+K9fcajkZ96gy5hCIyx1BV + a=ice-options:trickle + a=fingerprint:sha-256 88:A3:3E:2C:E3:3C:DF:E8:31:1B:59:AA:73:60:D8:EF:E7:FE:0D:F5:B8:F1:79:26:58:A3:D2:93:D9:8C:49:29 + a=setup:active + a=mid:audio + a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level + a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time + a=sendrecv + a=msid:stream0 audio0 + a=rtcp-mux + a=rtpmap:103 ISAC/16000 + a=rtpmap:104 ISAC/32000 + a=rtpmap:126 telephone-event/8000 + a=ssrc:666769703 cname:g5ORSLYV5oOfoEBX + a=ssrc:666769703 msid:stream0 audio0 + a=ssrc:666769703 mslabel:stream0 + a=ssrc:666769703 label:audio0 + */ + + 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] + ] + ), + 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" + ), + ] + + 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 + } + } + } + + let transport = ConferenceDescription.Transport( + 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 { @@ -249,7 +1146,7 @@ private func httpJsonRequest(url: String, method: HttpMethod, resultType: T.T request.httpBody = body request.httpMethod = "PATCH" - print("PATCH: \(String(data: body, encoding: .utf8)!)") + //print("PATCH: \(String(data: body, encoding: .utf8)!)") } let task = URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in @@ -279,25 +1176,28 @@ private func httpJsonRequest(url: String, method: HttpMethod, resultType: T.T public final class GroupCallContext { private final class Impl { - enum State { - case empty - case requestingConference - case allocatingChannels(conferenceId: String) - } - private let queue: Queue private let context: GroupCallThreadLocalContext private let disposable = MetaDisposable() - private var conferenceId: String? - private var audioChannelId: String? - private var audioChannelExpire: Int? - private var audioChannelEndpoint: String? - private var audioChannelDirection: String? + private let colibriHost: String + private let sessionId: UInt32 - init(queue: Queue) { + private var audioSessionDisposable: Disposable? + private let pollDisposable = MetaDisposable() + + private var conferenceId: String? + private var localBundleId: String? + private var localTransport: ConferenceDescription.Transport? + + init(queue: Queue, audioSessionActive: Signal) { self.queue = queue + self.sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) + //self.colibriHost = "192.168.8.118" + 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 @@ -313,259 +1213,252 @@ public final class GroupCallContext { strongSelf.relaySdpAnswer(sdpAnswer: sdpAnswer) } - self.requestConference() + 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://localhost:8080/colibri/conferences/", method: .get, resultType: [Any].self) + 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 conference = result.first as? [String: Any], let conferenceId = conference["id"] as? String { - strongSelf.allocateChannels(conferenceId: conferenceId) + 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://localhost:8080/colibri/conferences/", method: .post([:]), resultType: [String: Any].self) + 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 } - guard let conferenceId = result["id"] as? String else { - return + if let conference = ConferenceDescription(json: result) { + strongSelf.allocateChannels(conference: conference) } - strongSelf.allocateChannels(conferenceId: conferenceId) })) } })) } - func allocateChannels(conferenceId: String) { + private var currentSsrcOrder: [Int] = [] + + func allocateChannels(conference: ConferenceDescription) { let bundleId = UUID().uuidString - let audioChannelEndpoint = bundleId - let audioChannelExpire = 30 - let audioChannelDirection = "sendrecv" + 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: [] + ) - let payload: [String: Any] = [ - "id": conferenceId, - "contents": [ - [ - "name": "audio", - "channels": [ - [ - "expire": audioChannelExpire, - "initiator": true, - "endpoint": audioChannelEndpoint, - "direction": audioChannelDirection, - "channel-bundle-id": audioChannelEndpoint, - "rtp-level-relay-type": "mixer" - ] as [String: Any] - ] as [Any] - ] as [String: Any] - ] as [Any], - "channel-bundles": [ - [ - "id": "\(bundleId)", - "transport": [ - "xmlns": "urn:xmpp:jingle:transports:ice-udp:1", - "rtcp-mux": true - ] as [String: Any] - ] as [String: Any] - ] as [Any] - ] + 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: "" + ) + )) - self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId)", method: .patch(payload), resultType: [String: Any].self) + 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 channelBundles = result["channel-bundles"] as? [Any] else { - return - } - guard let channelBundle = channelBundles.first as? [String: Any] else { - return - } - guard let transport = channelBundle["transport"] as? [String: Any] else { - return - } - guard let contents = result["contents"] as? [Any] else { + + guard let conference = ConferenceDescription(json: result) else { return } - var audioChannelId: String? - for item in contents { - guard let item = item as? [String: Any] else { - continue - } - guard let channels = item["channels"] as? [Any] else { - continue - } - for channel in channels { - if let channel = channel as? [String: Any] { - if let id = channel["id"] as? String { - audioChannelId = id - } - } + var maybeTransport: ConferenceDescription.Transport? + for channelBundle in conference.channelBundles { + if channelBundle.id == bundleId { + maybeTransport = channelBundle.transport + break } } - let uniqueId = Int(Date().timeIntervalSince1970) - - var sdp = "" - func appendSdp(_ string: String) { - if !sdp.isEmpty { - sdp.append("\n") - } - sdp.append(string) - } - - appendSdp("v=0") - appendSdp("o=- \(uniqueId) 2 IN IP4 0.0.0.0") - appendSdp("s=-") - appendSdp("t=0 0") - appendSdp("a=group:BUNDLE audio") - appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") - appendSdp("c=IN IP4 0.0.0.0") - 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") - appendSdp("a=rtcp:1 IN IP4 0.0.0.0") - 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=setup:actpass") - appendSdp("a=mid:audio") - appendSdp("a=sendrecv") - - guard let ufrag = transport["ufrag"] as? String else { - return - } - guard let pwd = transport["pwd"] as? String else { + guard let transport = maybeTransport else { + assert(false) return } - appendSdp("a=ice-ufrag:\(ufrag)") - appendSdp("a=ice-pwd:\(pwd)") - - if let fingerprints = transport["fingerprints"] as? [Any] { - for fingerprint in fingerprints { - if let fingerprint = fingerprint as? [String: Any] { - guard let fingerprintValue = fingerprint["fingerprint"] as? String else { - continue - } - guard let hashMethod = fingerprint["hash"] as? String else { - continue - } - appendSdp("a=fingerprint:\(hashMethod) \(fingerprintValue)") - } - } + guard let (offerSdp, updatedOrder) = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: bundleId, bridgeHost: strongSelf.colibriHost, transport: transport, currentSsrcOrder: strongSelf.currentSsrcOrder) else { + return } + strongSelf.currentSsrcOrder = updatedOrder - if let candidates = transport["candidates"] as? [Any] { - for candidate in candidates { - if let candidate = candidate as? [String: Any] { - var candidateString = "a=candidate:" - guard let foundation = candidate["foundation"] as? String else { - continue - } - candidateString.append("\(foundation) ") - guard let component = candidate["component"] as? String else { - continue - } - candidateString.append("\(component) ") - guard var protocolValue = candidate["protocol"] as? String else { - continue - } - if protocolValue == "ssltcp" { - protocolValue = "tcp" - } - candidateString.append("\(protocolValue) ") - - guard let priority = candidate["priority"] as? String else { - continue - } - candidateString.append("\(priority) ") - - guard let ip = candidate["ip"] as? String else { - continue - } - //candidateString.append("\(ip) ") - candidateString.append("127.0.0.1 ") - - guard let port = candidate["port"] as? String else { - continue - } - candidateString.append("\(port) ") - - guard let type = candidate["type"] as? String else { - continue - } - candidateString.append("typ \(type) ") - - switch type { - case "srflx", "prflx", "relay": - if let relAddr = candidate["rel-addr"] as? String, let relPort = candidate["rel-port"] as? String { - candidateString.append("raddr \(relAddr) rport \(relPort) ") - } - break - default: - break - } - - if protocolValue == "tcp" { - guard let tcpType = candidate["tcptype"] as? String else { - continue - } - candidateString.append("tcptype \(tcpType) ") - } - - candidateString.append("generation ") - if let generation = candidate["generation"] as? String { - candidateString.append(generation) - } else { - candidateString.append("0") - } - - appendSdp(candidateString) - } - } - } + strongSelf.conferenceId = conference.id + strongSelf.localBundleId = bundleId + strongSelf.localTransport = transport - appendSdp("a=rtcp-mux") - appendSdp("") - - strongSelf.conferenceId = conferenceId - strongSelf.audioChannelId = audioChannelId - strongSelf.audioChannelExpire = audioChannelExpire - strongSelf.audioChannelEndpoint = audioChannelEndpoint - strongSelf.audioChannelDirection = audioChannelDirection - - strongSelf.context.setOfferSdp(sdp) + strongSelf.context.setOfferSdp(offerSdp) })) } private func relaySdpAnswer(sdpAnswer: String) { - guard let payload = convertSDPToColibri( - conferenceId: conferenceId!, - audioChannelId: audioChannelId!, - audioChannelExpire: audioChannelExpire!, - audioChannelEndpoint: audioChannelEndpoint!, - audioChannelDirection: audioChannelDirection!, - string: sdpAnswer - ) else { + guard let conferenceId = self.conferenceId, let localBundleId = self.localBundleId else { return } - self.disposable.set((httpJsonRequest(url: "http://localhost:8080/colibri/conferences/\(conferenceId!)", method: .patch(payload), resultType: [String: Any].self) + + 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 (offerSdp, updatedOrder) = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: localBundleId, bridgeHost: strongSelf.colibriHost, transport: localTransport, currentSsrcOrder: strongSelf.currentSsrcOrder) { + strongSelf.currentSsrcOrder = updatedOrder + strongSelf.context.setOfferSdp(offerSdp) + } + + strongSelf.pollOnceDelayed() })) } } @@ -573,10 +1466,10 @@ public final class GroupCallContext { private let queue = Queue() private let impl: QueueLocalObject - public init() { + public init(audioSessionActive: Signal) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue) + return Impl(queue: queue, audioSessionActive: audioSessionActive) }) } } From d593ef1261e03a3e1c766bb9934845235dbbf68b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 13 Oct 2020 23:01:53 +0400 Subject: [PATCH 04/12] Update submodule --- submodules/TgVoipWebrtc/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 4949dc52e3..710ba00fa7 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 4949dc52e358fe623229846fa78f72b7a6b55d16 +Subproject commit 710ba00fa7809526e1dbe912d0fd0f0e93a945fd From 531e88bf79282c3a6cec4479439bd22d84d0b9fb Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 15 Oct 2020 20:49:12 +0400 Subject: [PATCH 05/12] Trigger build --- Random.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Random.txt b/Random.txt index ca2dee0413..9a9364694b 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -8tRwQybvfoDddhSIfdMSOMv4FZd9LSHiWmObmx6d7rE= +gCh0ST/jBZ+NM8mvcBcsd12A5FMFT4q6fETcWd5elO0= From 258006cbe8784f1343be3d158378bc6c6efeafa7 Mon Sep 17 00:00:00 2001 From: Peter Iakovlev Date: Thu, 15 Oct 2020 23:07:49 +0000 Subject: [PATCH 06/12] Update ChatMessageBubbleItemNode.swift --- .../TelegramUI/Sources/ChatMessageBubbleItemNode.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 0e6558cf14..b9512c3553 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -604,9 +604,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) } @@ -2851,7 +2851,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 { @@ -2960,9 +2960,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 { From 799bae0cc2883e935208d9d29e3f1910b928497d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 15:10:19 +0400 Subject: [PATCH 07/12] WIP --- .../SettingsUI/Sources/DebugController.swift | 19 +- .../Sources/GroupCallController.swift | 109 ++++ .../Sources/GroupCallContext.swift | 541 ++++++++---------- .../GroupCallThreadLocalContext.h | 3 +- .../Sources/GroupCallThreadLocalContext.mm | 10 +- third-party/webrtc/BUILD | 2 +- 6 files changed, 379 insertions(+), 305 deletions(-) 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 index e69de29bb2..edd584d531 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallController.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallController.swift @@ -0,0 +1,109 @@ +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 let audioSessionActive = Promise(false) + + private var memberCount: Int = 0 + private let memberCountNode: ImmediateTextNode + + private var validLayout: ContainerViewLayout? + + init(context: AccountContext) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.memberCountNode = ImmediateTextNode() + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.memberCountNode) + + 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 + + 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) + } + }) + } + + deinit { + self.callDisposable?.dispose() + self.memberCountDisposable?.dispose() + } + + 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) + let textSize = self.memberCountNode.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: 100.0)) + transition.updateFrameAdditiveToCenter(node: self.memberCountNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: floor((layout.size.width - textSize.width) / 2.0)), size: textSize)) + } + } + + 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/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 3dbcfb216e..dd1352563f 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -497,6 +497,9 @@ private extension ConferenceDescription.Content.Channel.PayloadType { 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 } @@ -544,6 +547,7 @@ private extension ConferenceDescription.Content.Channel { if !self.rtpHdrExts.isEmpty { result["rtp-hdrexts"] = self.rtpHdrExts.map { $0.outgoingColibriDescription() } } + result["rtcp-mux"] = true return result } @@ -635,6 +639,21 @@ private extension ConferenceDescription.ChannelBundle { } } +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] = [:] @@ -646,283 +665,205 @@ private extension ConferenceDescription { return result } - func offerSdp(sessionId: UInt32, bundleId: String, bridgeHost: String, transport: ConferenceDescription.Transport, currentSsrcOrder: [Int]) -> (String, [Int])? { - var otherSsrc: [(Bool, Int, String)] = [] + 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 103 104 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 + 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") + 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 { - otherSsrc.append(contentsOf: channel.sources.map { ssrc in - return (true, ssrc, "stream0") + 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 { - otherSsrc.append(contentsOf: channel.ssrcs.map { ssrc in - return (false, ssrc, channel.channelBundleId) + precondition(channel.ssrcs.count <= 1) + ssrcList.append(contentsOf: channel.ssrcs.map { ssrc in + return Ssrc( + isMain: false, + value: ssrc, + streamId: "stream\(ssrc)", + isRemoved: false + ) }) } } } - otherSsrc.sort(by: { lhs, rhs in - /*if let previousLhsIndex = currentSsrcOrder.firstIndex(of: lhs.1), let previousRhsIndex = currentSsrcOrder.firstIndex(of: rhs.1) { - return previousLhsIndex < previousRhsIndex - } - if currentSsrcOrder.contains(lhs.1) != currentSsrcOrder.contains(rhs.1) { - return currentSsrcOrder.contains(lhs.1) - }*/ - if lhs.0 != rhs.0 { - return lhs.0 - } else { - return lhs.1 < rhs.1 - } - }) - var sdp = "" - func appendSdp(_ string: String) { - if !sdp.isEmpty { - sdp.append("\n") - } - sdp.append(string) + guard let mainSsrcId = maybeMainSsrcId else { + preconditionFailure() } - appendSdp("v=0") - appendSdp("o=- \(sessionId) 2 IN IP4 0.0.0.0") - appendSdp("s=-") - appendSdp("t=0 0") - - /*appendSdp("a=group:BUNDLE audio0") - do { - appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") - appendSdp("c=IN IP4 0.0.0.0") - 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") - appendSdp("a=rtcp:1 IN IP4 0.0.0.0") - 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=mid:audio0") - appendSdp("a=sendrecv") - 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)") + 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 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 - 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) - } - - for ssrc in bridgeSources { - appendSdp("a=ssrc:\(ssrc) cname:cname\(ssrc)") - appendSdp("a=ssrc:\(ssrc) msid:stream0 audio0") - appendSdp("a=ssrc:\(ssrc) mslabel:stream0") - appendSdp("a=ssrc:\(ssrc) label:audio0") - } - - /*for (ssrc, streamId) in otherSsrc { - appendSdp("a=ssrc:\(ssrc) cname:cname\(ssrc)") - appendSdp("a=ssrc:\(ssrc) msid:\(streamId) audio0") - //appendSdp("a=ssrc:\(ssrc) mslabel:\(streamId)") - //appendSdp("a=ssrc:\(ssrc) label:\(streamId)") - }*/ - - appendSdp("a=rtcp-mux") - }*/ - - appendSdp("a=group:BUNDLE audio") - appendSdp("a=ice-lite") - - appendSdp("a=msid-semantic:WMS *") - - appendSdp("m=audio 1 RTP/SAVPF 111 103 104 126") - - appendSdp("c=IN IP4 0.0.0.0") - - 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" + for ssrc in ssrcList { + if bundleSsrcs.contains(where: { $0.value == ssrc.value }) { + continue } - candidateString.append("\(protocolValue) ") - candidateString.append("\(candidate.priority) ") - - var ip = candidate.ip - 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 { + 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 } - candidateString.append("tcptype \(tcpType) ") + sdpList.append(createSdp(sessionId: sessionId, bundleSsrcs: [ssrc], isPartial: true)) } - - 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") - 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=mid:audio") - appendSdp("a=sendrecv") - - for (_, ssrc, streamId) in otherSsrc { - appendSdp("a=ssrc-group:FID \(ssrc)") - appendSdp("a=ssrc:\(ssrc) cname:stream\(streamId)") - appendSdp("a=ssrc:\(ssrc) msid:stream\(streamId) audio\(streamId)") - //appendSdp("a=ssrc:\(ssrc) mslabel:stream\(streamId)") - //appendSdp("a=ssrc:\(ssrc) label:audio\(streamId)") - } - - /*for (isBridge, ssrc, streamId) in otherSsrc { - let mPort: Int - if isBridge { - mPort = 1 - } else { - mPort = 0 - } - appendSdp("m=audio \(mPort) RTP/SAVPF 111 103 104 126") - appendSdp("c=IN IP4 0.0.0.0") - - if isBridge { - appendSdp("a=sendrecv") - } else { - appendSdp("a=bundle-only") - appendSdp("a=sendonly") - } - - 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") - appendSdp("a=rtcp:1 IN IP4 0.0.0.0") - 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=mid:audio\(ssrc)") - - if isBridge { - 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 - 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=ssrc:\(ssrc) cname:stream\(streamId)") - appendSdp("a=ssrc:\(ssrc) msid:stream\(streamId) audio\(streamId)") - //appendSdp("a=ssrc:\(ssrc) mslabel:stream\(streamId)") - //appendSdp("a=ssrc:\(ssrc) label:audio\(streamId)") - - appendSdp("a=rtcp-mux") }*/ - appendSdp("") - - return (sdp, otherSsrc.map(\.1)) + 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) { @@ -956,36 +897,6 @@ private extension ConferenceDescription { return result } - /* - v=0 - o=- 3432551037272164134 2 IN IP4 127.0.0.1 - s=- - t=0 0 - a=group:BUNDLE audio - a=msid-semantic: WMS stream0 - m=audio 9 RTP/SAVPF 103 104 126 - c=IN IP4 0.0.0.0 - a=rtcp:9 IN IP4 0.0.0.0 - a=ice-ufrag:XTZl - a=ice-pwd:GS+K9fcajkZ96gy5hCIyx1BV - a=ice-options:trickle - a=fingerprint:sha-256 88:A3:3E:2C:E3:3C:DF:E8:31:1B:59:AA:73:60:D8:EF:E7:FE:0D:F5:B8:F1:79:26:58:A3:D2:93:D9:8C:49:29 - a=setup:active - a=mid:audio - a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level - a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - a=sendrecv - a=msid:stream0 audio0 - a=rtcp-mux - a=rtpmap:103 ISAC/16000 - a=rtpmap:104 ISAC/32000 - a=rtpmap:126 telephone-event/8000 - a=ssrc:666769703 cname:g5ORSLYV5oOfoEBX - a=ssrc:666769703 msid:stream0 audio0 - a=ssrc:666769703 mslabel:stream0 - a=ssrc:666769703 label:audio0 - */ - var audioSources: [Int] = [] for line in getLines(prefix: "a=ssrc:") { let scanner = Scanner(string: line) @@ -1013,7 +924,10 @@ private extension ConferenceDescription { parameters: [ "fmtp": [ "minptime=10;useinbandfec=1" - ] as [Any] + ] as [Any], + "rtcp-fbs": [[ + "type": "transport-cc" + ] as [String: Any]] as [Any] ] ), ConferenceDescription.Content.Channel.PayloadType( @@ -1045,6 +959,10 @@ private extension ConferenceDescription { 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 { @@ -1190,12 +1108,14 @@ public final class GroupCallContext { private var localBundleId: String? private var localTransport: ConferenceDescription.Transport? + let memberCount = ValuePromise(0, ignoreRepeated: true) + init(queue: Queue, audioSessionActive: Signal) { self.queue = queue self.sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) - //self.colibriHost = "192.168.8.118" - self.colibriHost = "192.168.93.24" + self.colibriHost = "192.168.8.118" + //self.colibriHost = "192.168.93.24" //self.colibriHost = "51.104.206.109" var relaySdpAnswerImpl: ((String) -> Void)? @@ -1264,7 +1184,7 @@ public final class GroupCallContext { })) } - private var currentSsrcOrder: [Int] = [] + private var currentOfferState: RemoteOffer.State? func allocateChannels(conference: ConferenceDescription) { let bundleId = UUID().uuidString @@ -1368,16 +1288,22 @@ public final class GroupCallContext { return } - guard let (offerSdp, updatedOrder) = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: bundleId, bridgeHost: strongSelf.colibriHost, transport: transport, currentSsrcOrder: strongSelf.currentSsrcOrder) else { - return - } - strongSelf.currentSsrcOrder = updatedOrder - strongSelf.conferenceId = conference.id strongSelf.localBundleId = bundleId strongSelf.localTransport = transport - strongSelf.context.setOfferSdp(offerSdp) + //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) + } })) } @@ -1453,9 +1379,14 @@ public final class GroupCallContext { return } - if let (offerSdp, updatedOrder) = conference.offerSdp(sessionId: strongSelf.sessionId, bundleId: localBundleId, bridgeHost: strongSelf.colibriHost, transport: localTransport, currentSsrcOrder: strongSelf.currentSsrcOrder) { - strongSelf.currentSsrcOrder = updatedOrder - strongSelf.context.setOfferSdp(offerSdp) + 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() @@ -1472,4 +1403,16 @@ public final class GroupCallContext { 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 + } + } } diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h index 3e67424272..377a71d511 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h @@ -9,7 +9,8 @@ - (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer; -- (void)setOfferSdp:(NSString * _Nonnull)offerSdp; +- (void)emitOffer; +- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial; @end diff --git a/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm index fca53a91eb..b68d678a0d 100644 --- a/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm @@ -36,9 +36,15 @@ return self; } -- (void)setOfferSdp:(NSString * _Nonnull)offerSdp { +- (void)emitOffer { if (_instance) { - _instance->setOfferSdp([offerSdp UTF8String]); + _instance->emitOffer(); + } +} + +- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial { + if (_instance) { + _instance->setOfferSdp([offerSdp UTF8String], isPartial); } } 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", From c2724de5c3281656dd10956d196a2486c09b097e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 15:10:29 +0400 Subject: [PATCH 08/12] WIP --- submodules/TgVoipWebrtc/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 710ba00fa7..8d0707f074 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 710ba00fa7809526e1dbe912d0fd0f0e93a945fd +Subproject commit 8d0707f074999f77263ec32b3955e8f7f856cea4 From 5e9179bab38357ed9e08d4d1419d0d4b55304ce4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 17:26:19 +0400 Subject: [PATCH 09/12] WIP --- .../Sources/GroupCallController.swift | 42 ++++++++++- .../Sources/PeerInfo/PeerInfoScreen.swift | 18 ----- .../Sources/GroupCallContext.swift | 69 ++++++++++++++++--- .../GroupCallThreadLocalContext.h | 1 + .../Sources/GroupCallThreadLocalContext.mm | 6 ++ submodules/TgVoipWebrtc/tgcalls | 2 +- third-party/webrtc/webrtc-ios | 2 +- 7 files changed, 109 insertions(+), 31 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/GroupCallController.swift b/submodules/TelegramCallsUI/Sources/GroupCallController.swift index edd584d531..ae870995de 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallController.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallController.swift @@ -17,11 +17,16 @@ public final class GroupCallController: ViewController { 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) { @@ -29,6 +34,9 @@ public final class GroupCallController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.memberCountNode = ImmediateTextNode() + self.isMutedNode = ImmediateTextNode() + + self.muteButton = HighlightableButtonNode() super.init() @@ -36,6 +44,9 @@ public final class GroupCallController: ViewController { 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 }) @@ -51,7 +62,7 @@ public final class GroupCallController: ViewController { let callContext = GroupCallContext(audioSessionActive: self.audioSessionActive.get()) self.callContext = callContext - memberCountDisposable = (callContext.memberCount + self.memberCountDisposable = (callContext.memberCount |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return @@ -61,6 +72,19 @@ public final class GroupCallController: ViewController { 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 { @@ -68,12 +92,26 @@ public final class GroupCallController: ViewController { 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)) - transition.updateFrameAdditiveToCenter(node: self.memberCountNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: floor((layout.size.width - textSize.width) / 2.0)), size: textSize)) + 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) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1d2637d9ce..ad5f3a1238 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3158,24 +3158,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private var groupCall: GroupCallContext? private func requestCall(isVideo: Bool) { - #if DEBUG - - let audioSessionActive = Promise(false) - self.groupCallDisposable.set(self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] audioSessionControl in - audioSessionControl.activate({ _ in }) - audioSessionActive.set(.single(true)) - }, deactivate: { - return Signal { subscriber in - subscriber.putCompletion() - return EmptyDisposable - } - }, availableOutputsChanged: { _, _ in - })) - - self.groupCall = GroupCallContext(audioSessionActive: audioSessionActive.get()) - return; - #endif - guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { return } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index dd1352563f..0e57845a60 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -691,7 +691,7 @@ private extension ConferenceDescription { appendSdp("a=ice-lite") for ssrc in bundleSsrcs { - appendSdp("m=audio \(ssrc.isMain ? "1" : "0") RTP/SAVPF 111 103 104 126") + appendSdp("m=audio \(ssrc.isMain ? "1" : "0") RTP/SAVPF 111 126") if ssrc.isMain { appendSdp("c=IN IP4 0.0.0.0") } @@ -722,7 +722,9 @@ private extension ConferenceDescription { candidateString.append("\(candidate.priority) ") var ip = candidate.ip - ip = bridgeHost + if ip.hasPrefix("192.") { + ip = bridgeHost + } candidateString.append("\(ip) ") candidateString.append("\(candidate.port) ") @@ -752,10 +754,10 @@ private extension ConferenceDescription { } appendSdp("a=rtpmap:111 opus/48000/2") - appendSdp("a=rtpmap:103 ISAC/16000") - appendSdp("a=rtpmap:104 ISAC/32000") + //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") + 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") @@ -930,7 +932,7 @@ private extension ConferenceDescription { ] as [String: Any]] as [Any] ] ), - ConferenceDescription.Content.Channel.PayloadType( + /*ConferenceDescription.Content.Channel.PayloadType( id: 103, name: "ISAC", clockrate: 16000, @@ -941,7 +943,7 @@ private extension ConferenceDescription { name: "ISAC", clockrate: 32000, channels: 1 - ), + ),*/ ConferenceDescription.Content.Channel.PayloadType( id: 126, name: "telephone-event", @@ -996,8 +998,30 @@ private extension ConferenceDescription { } } + 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: candidates, fingerprints: fingerprints, ufrag: ufrag, pwd: pwd @@ -1110,11 +1134,14 @@ public final class GroupCallContext { 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 = "192.168.8.118" + self.colibriHost = "51.11.141.27" //self.colibriHost = "192.168.93.24" //self.colibriHost = "51.104.206.109" @@ -1392,6 +1419,12 @@ public final class GroupCallContext { strongSelf.pollOnceDelayed() })) } + + func toggleIsMuted() { + self.isMutedValue = !self.isMutedValue + self.isMuted.set(self.isMutedValue) + self.context.setIsMuted(self.isMutedValue) + } } private let queue = Queue() @@ -1415,4 +1448,22 @@ public final class GroupCallContext { 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 index 377a71d511..ba75ba664a 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/GroupCallThreadLocalContext.h @@ -11,6 +11,7 @@ - (void)emitOffer; - (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial; +- (void)setIsMuted:(bool)isMuted; @end diff --git a/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm index b68d678a0d..0cf157d985 100644 --- a/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/GroupCallThreadLocalContext.mm @@ -48,4 +48,10 @@ } } +- (void)setIsMuted:(bool)isMuted { + if (_instance) { + _instance->setIsMuted(isMuted); + } +} + @end diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 8d0707f074..4f7501d281 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 8d0707f074999f77263ec32b3955e8f7f856cea4 +Subproject commit 4f7501d281b851e6302b2a2d7298c733eee82414 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 From 9fa15d5765ec4d0bd75a0d83571499a186d1b0c6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 18:39:29 +0400 Subject: [PATCH 10/12] [WIP] Pinned messages --- .../AccountContext/Sources/MediaManager.swift | 15 ++-- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api1.swift | 64 ++++++++++-------- .../Sources/AccountStateManagementUtils.swift | 4 +- .../Sources/AccountViewTracker.swift | 8 +-- .../Sources/HistoryViewStateValidation.swift | 20 +++--- submodules/TelegramCore/Sources/Holes.swift | 12 ++-- .../Sources/LoadMessagesIfNecessary.swift | 4 +- .../Sources/LoadedPeerFromMessage.swift | 4 +- .../Sources/RequestUserPhotos.swift | 4 +- .../TelegramCore/Sources/SearchMessages.swift | 24 +++---- .../Sources/SingleMessageView.swift | 4 +- .../Sources/SynchronizePeerReadState.swift | 4 +- .../PresentationThemeEssentialGraphics.swift | 16 +++++ .../Chat/Message/Contents.json | 6 +- .../Message/Pinned.imageset/Contents.json | 12 ++++ .../Message/Pinned.imageset/messagepin.pdf | Bin 0 -> 5879 bytes .../TelegramUI/Sources/ChatController.swift | 16 ++++- .../ChatInterfaceStateContextMenus.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../ChatMessageAttachedContentNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 4 +- .../ChatMessageContactBubbleContentNode.swift | 2 +- .../ChatMessageDateAndStatusNode.swift | 26 +++++-- .../ChatMessageInteractiveFileNode.swift | 2 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../ChatMessageMapBubbleContentNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 2 +- .../ChatMessagePollBubbleContentNode.swift | 2 +- ...atMessageRestrictedBubbleContentNode.swift | 2 +- .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../ChatMessageTextBubbleContentNode.swift | 2 +- .../ChatPanelInterfaceInteraction.swift | 4 +- .../ChatPinnedMessageTitlePanelNode.swift | 2 +- .../Sources/ChatRecentActionsController.swift | 2 +- .../TelegramUI/Sources/OpenChatMessage.swift | 6 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/PeerMessagesMediaPlaylist.swift | 34 +++++----- 38 files changed, 195 insertions(+), 130 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/Pinned.imageset/messagepin.pdf 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/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 8ad7e05cff..1a60414e04 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -541,8 +541,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1963717851] = { return Api.WallPaper.parse_wallPaperNoFile($0) } dict[-1938715001] = { return Api.messages.Messages.parse_messages($0) } dict[1951620897] = { return Api.messages.Messages.parse_messagesNotModified($0) } - dict[-1725551049] = { return Api.messages.Messages.parse_channelMessages($0) } - dict[-923939298] = { return Api.messages.Messages.parse_messagesSlice($0) } + dict[1682413576] = { return Api.messages.Messages.parse_channelMessages($0) } + dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } dict[-1022713000] = { return Api.Invoice.parse_invoice($0) } dict[1933519201] = { return Api.PeerSettings.parse_peerSettings($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 6adfb5aa76..6840de7b7d 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1093,8 +1093,8 @@ public struct messages { public enum Messages: TypeConstructorDescription { case messages(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) case messagesNotModified(count: Int32) - case channelMessages(flags: Int32, pts: Int32, count: Int32, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case channelMessages(flags: Int32, pts: Int32, count: Int32, offsetIdOffset: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, offsetIdOffset: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1124,13 +1124,14 @@ public struct messages { } serializeInt32(count, buffer: buffer, boxed: false) break - case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): + case .channelMessages(let flags, let pts, let count, let offsetIdOffset, let messages, let chats, let users): if boxed { - buffer.appendInt32(-1725551049) + buffer.appendInt32(1682413576) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -1147,13 +1148,14 @@ public struct messages { item.serialize(buffer, true) } break - case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): + case .messagesSlice(let flags, let count, let nextRate, let offsetIdOffset, let messages, let chats, let users): if boxed { - buffer.appendInt32(-923939298) + buffer.appendInt32(978610270) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextRate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -1179,10 +1181,10 @@ public struct messages { return ("messages", [("messages", messages), ("chats", chats), ("users", users)]) case .messagesNotModified(let count): return ("messagesNotModified", [("count", count)]) - case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): - return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("messages", messages), ("chats", chats), ("users", users)]) - case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): - return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("messages", messages), ("chats", chats), ("users", users)]) + case .channelMessages(let flags, let pts, let count, let offsetIdOffset, let messages, let chats, let users): + return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("offsetIdOffset", offsetIdOffset), ("messages", messages), ("chats", chats), ("users", users)]) + case .messagesSlice(let flags, let count, let nextRate, let offsetIdOffset, let messages, let chats, let users): + return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("offsetIdOffset", offsetIdOffset), ("messages", messages), ("chats", chats), ("users", users)]) } } @@ -1227,26 +1229,29 @@ public struct messages { _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: [Api.Message]? + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: [Api.Message]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) } - var _5: [Api.Chat]? + var _6: [Api.Chat]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _6: [Api.User]? + var _7: [Api.User]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, messages: _4!, chats: _5!, users: _6!) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, offsetIdOffset: _4, messages: _5!, chats: _6!, users: _7!) } else { return nil @@ -1259,26 +1264,29 @@ public struct messages { _2 = reader.readInt32() var _3: Int32? if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: [Api.Message]? + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: [Api.Message]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) } - var _5: [Api.Chat]? + var _6: [Api.Chat]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _6: [Api.User]? + var _7: [Api.User]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = _4 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, messages: _4!, chats: _5!, users: _6!) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, offsetIdOffset: _4, messages: _5!, chats: _6!, users: _7!) } else { return nil diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index ef69bac4b1..fd260485c3 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1461,9 +1461,9 @@ private func resolveAssociatedMessages(network: Network, state: AccountMutableSt switch result { case let .messages(messages, chats, users): return (messages, chats, users) - case let .messagesSlice(_, _, _, messages, chats, users): + case let .messagesSlice(_, _, _, _, messages, chats, users): return (messages, chats, users) - case let .channelMessages(_, _, _, messages, chats, users): + case let .channelMessages(_, _, _, _, messages, chats, users): return (messages, chats, users) case .messagesNotModified: return ([], [], []) diff --git a/submodules/TelegramCore/Sources/AccountViewTracker.swift b/submodules/TelegramCore/Sources/AccountViewTracker.swift index cf87f6e9d8..3572efe0b5 100644 --- a/submodules/TelegramCore/Sources/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/AccountViewTracker.swift @@ -90,11 +90,11 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal map { result -> Set in let apiMessages: [Api.Message] switch result { - case let .channelMessages(_, _, _, messages, _, _): + case let .channelMessages(_, _, _, _, messages, _, _): apiMessages = messages case let .messages(messages, _, _): apiMessages = messages - case let .messagesSlice(_, _, _, messages, _, _): + case let .messagesSlice(_, _, _, _, messages, _, _): apiMessages = messages case .messagesNotModified: return Set() @@ -912,11 +912,11 @@ private func validateReplyThreadBatch(postbox: Postbox, network: Network, transa |> map { result -> Set in let apiMessages: [Api.Message] switch result { - case let .channelMessages(_, _, _, messages, _, _): + case let .channelMessages(_, _, _, _, messages, _, _): apiMessages = messages case let .messages(messages, _, _): apiMessages = messages - case let .messagesSlice(_, _, _, messages, _, _): + case let .messagesSlice(_, _, _, _, messages, _, _): apiMessages = messages case .messagesNotModified: return Set() diff --git a/submodules/TelegramCore/Sources/Holes.swift b/submodules/TelegramCore/Sources/Holes.swift index f571e162e5..beca7c99ba 100644 --- a/submodules/TelegramCore/Sources/Holes.swift +++ b/submodules/TelegramCore/Sources/Holes.swift @@ -80,9 +80,9 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis switch result { case let .messages(messages, chats, users): return (messages, chats, users) - case let .messagesSlice(_, _, _, messages, chats, users): + case let .messagesSlice(_, _, _, _, messages, chats, users): return (messages, chats, users) - case let .channelMessages(_, _, _, messages, chats, users): + case let .channelMessages(_, _, _, _, messages, chats, users): return (messages, chats, users) case .messagesNotModified: return ([], [], []) @@ -436,11 +436,11 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH messages = apiMessages chats = apiChats users = apiUsers - case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + case let .messagesSlice(_, _, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): messages = apiMessages chats = apiChats users = apiUsers - case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, pts, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -670,11 +670,11 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId messages = apiMessages chats = apiChats users = apiUsers - case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + case let .messagesSlice(_, _, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): messages = apiMessages chats = apiChats users = apiUsers - case let .channelMessages(_, _, _, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers diff --git a/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift b/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift index deba93d87b..66ffa33865 100644 --- a/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift +++ b/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift @@ -57,9 +57,9 @@ public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postb switch result { case let .messages(messages, chats, users): return (messages, chats, users) - case let .messagesSlice(_, _, _, messages, chats, users): + case let .messagesSlice(_, _, _, _, messages, chats, users): return (messages, chats, users) - case let .channelMessages(_, _, _, messages, chats, users): + case let .channelMessages(_, _, _, _, messages, chats, users): return (messages, chats, users) case .messagesNotModified: return ([], [], []) diff --git a/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift b/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift index 7137701715..6234fd44bc 100644 --- a/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift +++ b/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift @@ -37,9 +37,9 @@ public func loadedPeerFromMessage(account: Account, peerId: PeerId, messageId: M switch result { case let .messages(_, _, users): apiUsers = users - case let .messagesSlice(_, _, _, _, _, users): + case let .messagesSlice(_, _, _, _, _, _, users): apiUsers = users - case let .channelMessages(_, _, _, _, _, users): + case let .channelMessages(_, _, _, _, _, _, users): apiUsers = users case .messagesNotModified: apiUsers = [] diff --git a/submodules/TelegramCore/Sources/RequestUserPhotos.swift b/submodules/TelegramCore/Sources/RequestUserPhotos.swift index 5d4f40d963..9389af3634 100644 --- a/submodules/TelegramCore/Sources/RequestUserPhotos.swift +++ b/submodules/TelegramCore/Sources/RequestUserPhotos.swift @@ -72,7 +72,7 @@ public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId let chats: [Api.Chat] let users: [Api.User] switch result { - case let .channelMessages(_, _, _, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -80,7 +80,7 @@ public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId messages = apiMessages chats = apiChats users = apiUsers - case let .messagesSlice(_, _, _, apiMessages, apiChats, apiUsers): + case let .messagesSlice(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers diff --git a/submodules/TelegramCore/Sources/SearchMessages.swift b/submodules/TelegramCore/Sources/SearchMessages.swift index 9cbc767ec6..a95990e46e 100644 --- a/submodules/TelegramCore/Sources/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/SearchMessages.swift @@ -71,7 +71,7 @@ private func mergedState(transaction: Transaction, state: SearchMessagesPeerStat let totalCount: Int32 let nextRate: Int32? switch result { - case let .channelMessages(_, _, count, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, _, count, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -83,7 +83,7 @@ private func mergedState(transaction: Transaction, state: SearchMessagesPeerStat users = apiUsers totalCount = Int32(messages.count) nextRate = nil - case let .messagesSlice(_, count, apiNextRate, apiMessages, apiChats, apiUsers): + case let .messagesSlice(_, count, _, apiNextRate, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -412,7 +412,7 @@ public func downloadMessage(postbox: Postbox, network: Network, messageId: Messa let chats: [Api.Chat] let users: [Api.User] switch result { - case let .channelMessages(_, _, _, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -420,7 +420,7 @@ public func downloadMessage(postbox: Postbox, network: Network, messageId: Messa messages = apiMessages chats = apiChats users = apiUsers - case let .messagesSlice(_, _, _, apiMessages, apiChats, apiUsers): + case let .messagesSlice(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -497,7 +497,7 @@ func fetchRemoteMessage(postbox: Postbox, source: FetchMessageHistoryHoleSource, let chats: [Api.Chat] let users: [Api.User] switch result { - case let .channelMessages(_, _, _, apiMessages, apiChats, apiUsers): + case let .channelMessages(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -505,7 +505,7 @@ func fetchRemoteMessage(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages = apiMessages chats = apiChats users = apiUsers - case let .messagesSlice(_, _, _, apiMessages, apiChats, apiUsers): + case let .messagesSlice(_, _, _, _, apiMessages, apiChats, apiUsers): messages = apiMessages chats = apiChats users = apiUsers @@ -574,9 +574,9 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, threadI switch result { case let .messages(apiMessages, _, _): messages = apiMessages - case let .channelMessages(_, _, _, apiMessages, _, _): + case let .channelMessages(_, _, _, _, apiMessages, _, _): messages = apiMessages - case let .messagesSlice(_, _, _, apiMessages, _, _): + case let .messagesSlice(_, _, _, _, apiMessages, _, _): messages = apiMessages case .messagesNotModified: messages = [] @@ -604,9 +604,9 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, threadI switch result { case let .messages(apiMessages, _, _): messages = apiMessages - case let .channelMessages(_, _, _, apiMessages, _, _): + case let .channelMessages(_, _, _, _, apiMessages, _, _): messages = apiMessages - case let .messagesSlice(_, _, _, apiMessages, _, _): + case let .messagesSlice(_, _, _, _, apiMessages, _, _): messages = apiMessages case .messagesNotModified: messages = [] @@ -628,9 +628,9 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, threadI switch result { case let .messages(apiMessages, _, _): messages = apiMessages - case let .channelMessages(_, _, _, apiMessages, _, _): + case let .channelMessages(_, _, _, _, apiMessages, _, _): messages = apiMessages - case let .messagesSlice(_, _, _, apiMessages, _, _): + case let .messagesSlice(_, _, _, _, apiMessages, _, _): messages = apiMessages case .messagesNotModified: messages = [] diff --git a/submodules/TelegramCore/Sources/SingleMessageView.swift b/submodules/TelegramCore/Sources/SingleMessageView.swift index f54bc1bc62..203daf6bce 100644 --- a/submodules/TelegramCore/Sources/SingleMessageView.swift +++ b/submodules/TelegramCore/Sources/SingleMessageView.swift @@ -57,11 +57,11 @@ private func fetchMessage(transaction: Transaction, account: Account, messageId: apiMessages = messages apiChats = chats apiUsers = users - case let .messagesSlice(_, _, _, messages, chats, users): + case let .messagesSlice(_, _, _, _, messages, chats, users): apiMessages = messages apiChats = chats apiUsers = users - case let .channelMessages(_, _, _, messages, chats, users): + case let .channelMessages(_, _, _, _, messages, chats, users): apiMessages = messages apiChats = chats apiUsers = users diff --git a/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift b/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift index 9817e3c28b..ec2773ead4 100644 --- a/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift +++ b/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift @@ -48,11 +48,11 @@ private func dialogTopMessage(network: Network, postbox: Postbox, peerId: PeerId } let apiMessages: [Api.Message] switch result { - case let .channelMessages(_, _, _, messages, _, _): + case let .channelMessages(_, _, _, _, messages, _, _): apiMessages = messages case let .messages(messages, _, _): apiMessages = messages - case let .messagesSlice(_, _, _, messages, _, _): + case let .messagesSlice(_, _, _, _, messages, _, _): apiMessages = messages case .messagesNotModified: apiMessages = [] 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 0000000000000000000000000000000000000000..a42a8d6261dafbb41015acbc11eaf6817c31ac3b GIT binary patch literal 5879 zcmbtYXH*mG)}|>C6i}KJAtIrd1PBljkX|KJB~%j#9ipLwh#*CpG?Ai!^d^Xc(xi7O zkq#mt(z}#H6}gG!Ts@v|-5=kqSu?ZtJa60knLYb`Ksrh)LJ(mw0H}F(b9Om*>*>4Z zR)9DV48)-90hccWMYPb)Hm*m3C&7rw!baQEpRXKR9YXQ+bi2UV(G(wCFBPo_Peth?PfOYJO@#s)u)u$4jwK zg4%c9HV-aDZreVr(qms{0IEmo3~mJojVtZIC9ldhvl zlRLhMmwIBcFa?ql;2WKoB%It|`T$u$T|f~S7{#OQ@)snu194W(HV_XZRwq^g3%a(kw$KOY2Xrn8;(-TyS_{}U2nxiX5DEN=pU;2S1^YgRDi-5*^V{_Ffg%Q2q_fLSB;FC0UO*8=eINu(KmkP5 zfv#9LypuG6)<{Pe!Z-2TXCYufVZdtuCQ?vTq~+Hzq`2O$4}K@6-)`{ELs&vjP57iH zD=$5dz(Vo@^J8NF%7ApfM09a970S4A-xNkGtCO9rKplHB?@F*jvI3CkLn<@i&rlM4 zLU5Cm_)pIhR`Ante|laK3=^cl%-hX%2%!VnqfS< zEZHQk%z@?Q=pdtfXG z?4#=4=g?O{TI7?(W4&yjEtS5eL@wPX^OU?nVV704m!cp4rSxOD|8c8P z7u({BnSRkZZ*}R0$>Pst3z++Adg9V4ciFU9`?zYhwjl?yR3UtC0H<cVn{K!_^eC zA_a8GNi&Z!{uTueUD8zkZSs+@W&QMXYb+n%9#7|{bVfc%V_`)iUo#yajtGw7az;<_ zt?=!LOV@w8VKX8-?7`?$#SnVi8K^Oa>%8B7ew@U zi88r>HW~W|i%QCe6OCZ}m;{bT)0@=$x?V$ue4F<#gK}rgN*q0K-H$QZ<=E|6-VC=x zmwBU4h+b2;6zSwqB2jnvh$QJ&yHV!!XS<9^)WoSvluqk$!}<4vM(8y$No#hHEwjl6 zB`|wL^w|&RpHOO_prQQW`{-WvjmHhi3*D!;ruYx|XC>q=)bh|54#Ja-D^UVIjZ-}F z6tcdi>a0sM6?*U7S|Et94k2d=uUoOr9{MX6tCC!5Lu#F_U+%dRYJP9Q)2gPxy}9Bi!U{NcBOV}(|IQ>>^uPvwT zniO@@eDu4w5b%}bY1(bWKa<|KOhm|W5U?cdXWsjklnF-sZ$@2FQ32_KwgP_3V|qZd zzd`>}y#=+;E?Q5#}%biRCe)Mnjx zxQa;UO8{A;s}b>C@pL2lwdDrWvvi%!yCmmmiK&f<>!+-($yE20NCv*t$Rs7@skN5w ztO?(>svZ-p)8Td*I`)g9{n`xHhp?+x#v*)HV zGtTMe=Z-tMa{|>Ji3!{@=`4sI<~QQ05!E^q$)Kss2(on3M9@c@%-iJX8UyuYqS;;E z{{FouT0~&_B#W+j?sUgcQDu7Owa`rxB8rF2us2xh@O_wVDv@W7%}f|ss5&a=%y6RJ z>_WTs!A7b6J_;i6u<7d5eNQdicG;;1V5YmzF^`mqK?tWL+nCiNw$)X{(46gl-RLCy z9sMJjcJD*O4ek|Os(eH$V$yNtnMe&I(GHjK>imbc%;6d}YPag*eSYSYSc&Ff9P{wk{a9o7A~>cuhKoNzVVGI)fNmc{V3E zM^mY*>lAeY&gy8F_ISD;X4l=(B={t)o@pa!!BBVMA!T5wZFSIPf1_w&D?p&%;FRAv z2_AChJ1GwIwr&3_<41$w0>jlC#m|u1^8t{=#4q!5gR9?uy+cod6OA-c_R| zeMBm(Mq+n`?4`Wy2oXRYGD7Tr&;BOSfWP-UGVK6~nJ>n6gC>}G z!rzV};4>{lhI~OZV-`)4!VoR1F@=|+yp9w-X^UcLG;Qpa{de4X=lJgxq$}#kY+rO` zsiH8uckq<_9Z>w_5wUOJnM9I)Q5l1&0{UERuZX1iS6c{igTu-&d_4Aa+=|Y>buKLr>D3#Jm2t7(60y3G`M_5 z-M&x@@MH61pN!&Yt>cNNG$wNhptMK0 z@nA&$jXE2*b{xHWFc*^^7hh7Dzk#|vrx~{lcg=;kj!Ram*mH<0=I~;!G@u2DCRR@8 zAul&QT!T$nVN@HYQF+DdikD)x-swE&0i%M%suW{*jgqg1uZU`#Si)AqbVpHKVT?ok za)J*C{%|ioJT4y#X|ge=1a&@NmZzK#ew}_ zO-nLMaV*I!wJgjmhAauzk!4<~kxiy{X%C%Qtgwlse7R}4zJ~%_Q^ZKb;<@g6x~y(mQCe1= zYMqJS+xmCF_cy}jA2;Nsho#pcQ<0fdmKO_R3VZo8jWeZNr7U0H3fqvKHsLM_O;L|k zkLK2(50*DEWXuJX0BZ3TwLDemm3a@xB0+Ot?{a3gbAV zh2W~7zRG`UX;04h+^eE|p-K0;RbT03vyySf zYVB&=4)qS_A@?DKTqeXbq=tHT#MPa*I?#9U%<{riMbAdWG&zqC&x?Nb+{qL~4`0ve zo=mD!OrMzsgwA1mZ%tLOVPk}?gd;3GZ>A3zj>S(KFCTPW9gduLZ+2=PACVb7syGy& z(WHq{4R3px8#m8_qcXpr@ceqr_2=cWyQp>h^<3FYvZ1n(vUIiXwS~SSd&nb_Blm;x zt>Mq}2g8SOGAXiWlna0?G8M9G!$1K@!Ty@A{?0l=heB_4N9{NiRSwi5;z+R<{FD*9xXYwCLKg{G4%kRzawhJuVE9+MPA;U@p%*14PYDAjEGr@{g zZ|w%x4Cyki^u}oMx*)8p6CZrl`b#Z>>oMpaf~wU1<%EqYI_<4)*O|HPndBMg&hs5x z9fxOn_Bm;!CxAnYYx;!K|{r+W0l$-W(`$Sf7LEWIRTy2Mn89|bVj0CC!ND4 z26Uh5mek&`EnS~$Rj5?ZovIP)F`;&Bab$2@9k(tmvL8#}H1GWj)%hvx6CCs*c6k!v zdwqYrYbS8ECVMCQ(Bq>!g~x~S#i}i%BoxAY^j+~sv!dCq&5X_R>gy}D*N`T+ZqV38 zSw)qJ=Dn=Zcy6ycn7fp<iQ@)z=zHMx{ZqmuGpE#1Vgu{d zk-Sq2^25}#CfstyQdOOepM{^&s@g^jPBln1yR9+GZBTk(STaxYp+sY3Z0n2ZTWy%5 zkAErp&=oG~h-Fspkx5;EZG39m<5*8Ub$Qe)%(q~X=O}IG^*iG*0293TucwclgF?$k2{+Rtw9^%p-dr{uDi-smCf{} z6qgiFbowVeH{Ynl=8x4Jelbiav?+#}N&BYw)~p5}MGmo7r#`Y+xnsBZYTc=3@n-4K zqv@gtwI_V%56lilCU&ZPXCEA=u#$$KJXjO1^*J{GAhYN@anwtmPh&3EZu#69kO@g?#v+6!J5`Dc%cNLQpI#^$>$QEqxG@;agFo@Os~9T+1o(eL{wq;=V$s$B zyjvsz;J*)`1QaR;1zH1t*r1{WUl0zU^G_QXDu$2B?=~n{@(&vrEQ)vS?>I1&up{}0 zO@g4n{L?1+=eV$c83QJT*L!|%D~c!3KW$L37@jo0;~-!tUOoEVCMqh9r^-KVus?86 zFkuh&I}Qqg{<1b#EYi*qjs2!3>)Uyw@qWhZY#0olc?5RhRb_Q&YYd)--=CH6M7%7D zloE%5p)hfnl^DzlDlQHdw?e`!Ma3*p)(~q+lq}$XL%tK)#TCz?ZvlpiNr?hLAZ54; F;6Egz`8EIm literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0bd71dc3ee..913995ac1e 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4882,7 +4882,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 @@ -4904,7 +4904,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 { @@ -4915,7 +4918,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/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 b9512c3553..c3bd2abef8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -900,7 +900,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?, @@ -1437,7 +1437,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)) } } 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 f4ac6a8238..dc0ef5127a 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -143,7 +143,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) { @@ -152,7 +152,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 { @@ -163,7 +163,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 6b0d5bff27..41c3665781 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -405,7 +405,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { return false }, unblockPeer: { }, pinMessage: { _ in - }, unpinMessage: { _ in + }, unpinMessage: { _, _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { 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() } } } From bafc72569764a2616c9d2bc5402eda71b061a753 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 21:44:06 +0400 Subject: [PATCH 11/12] [WIP] Pinned messages --- .../TelegramUI/Sources/ChatController.swift | 52 +++++++++++++++++-- .../Sources/ChatHistoryListNode.swift | 2 + 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 913995ac1e..c700d4616b 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 @@ -3653,6 +3694,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 { 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 From 1020c3c4ae228eb428cafabf71651297b901b078 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 16 Oct 2020 23:37:17 +0400 Subject: [PATCH 12/12] Update API --- submodules/TelegramApi/Sources/Api0.swift | 3 +- submodules/TelegramApi/Sources/Api1.swift | 96 ++++--- .../Sources/AccountStateManagementUtils.swift | 27 +- .../TelegramCore/Sources/UpdateGroup.swift | 2 + .../Sources/UpdatePinnedMessage.swift | 244 +++++------------- .../TelegramUI/Sources/ChatController.swift | 14 +- 6 files changed, 126 insertions(+), 260 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1a60414e04..a2840ab313 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -231,10 +231,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1893427255] = { return Api.Update.parse_updateChannelAvailableMessages($0) } dict[-513517117] = { return Api.Update.parse_updateDialogUnreadMark($0) } dict[1180041828] = { return Api.Update.parse_updateLangPackTooLong($0) } - dict[1279515160] = { return Api.Update.parse_updateUserPinnedMessage($0) } dict[-1398708869] = { return Api.Update.parse_updateMessagePoll($0) } dict[1421875280] = { return Api.Update.parse_updateChatDefaultBannedRights($0) } - dict[-519195831] = { return Api.Update.parse_updateChatPinnedMessage($0) } dict[422972864] = { return Api.Update.parse_updateFolderPeers($0) } dict[1852826908] = { return Api.Update.parse_updateDialogPinned($0) } dict[-99664734] = { return Api.Update.parse_updatePinnedDialogs($0) } @@ -258,6 +256,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1178116716] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) } dict[610945826] = { return Api.Update.parse_updatePeerBlocked($0) } dict[-13975905] = { return Api.Update.parse_updateChannelUserTyping($0) } + dict[-309990731] = { return Api.Update.parse_updatePinnedMessages($0) } dict[-2054649973] = { return Api.Update.parse_updatePinnedChannelMessages($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 6840de7b7d..ef5d09c5d1 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -6166,10 +6166,8 @@ public extension Api { case updateChannelAvailableMessages(channelId: Int32, availableMinId: Int32) case updateDialogUnreadMark(flags: Int32, peer: Api.DialogPeer) case updateLangPackTooLong(langCode: String) - case updateUserPinnedMessage(userId: Int32, id: Int32) case updateMessagePoll(flags: Int32, pollId: Int64, poll: Api.Poll?, results: Api.PollResults) case updateChatDefaultBannedRights(peer: Api.Peer, defaultBannedRights: Api.ChatBannedRights, version: Int32) - case updateChatPinnedMessage(chatId: Int32, id: Int32, version: Int32) case updateFolderPeers(folderPeers: [Api.FolderPeer], pts: Int32, ptsCount: Int32) case updateDialogPinned(flags: Int32, folderId: Int32?, peer: Api.DialogPeer) case updatePinnedDialogs(flags: Int32, folderId: Int32?, order: [Api.DialogPeer]?) @@ -6193,6 +6191,7 @@ public extension Api { case updateReadChannelDiscussionOutbox(channelId: Int32, topMsgId: Int32, readMaxId: Int32) case updatePeerBlocked(peerId: Api.Peer, blocked: Api.Bool) case updateChannelUserTyping(flags: Int32, channelId: Int32, topMsgId: Int32?, userId: Int32, action: Api.SendMessageAction) + case updatePinnedMessages(flags: Int32, peer: Api.Peer, messages: [Int32], pts: Int32, ptsCount: Int32) case updatePinnedChannelMessages(flags: Int32, channelId: Int32, messages: [Int32], pts: Int32, ptsCount: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -6677,13 +6676,6 @@ public extension Api { } serializeString(langCode, buffer: buffer, boxed: false) break - case .updateUserPinnedMessage(let userId, let id): - if boxed { - buffer.appendInt32(1279515160) - } - serializeInt32(userId, buffer: buffer, boxed: false) - serializeInt32(id, buffer: buffer, boxed: false) - break case .updateMessagePoll(let flags, let pollId, let poll, let results): if boxed { buffer.appendInt32(-1398708869) @@ -6701,14 +6693,6 @@ public extension Api { defaultBannedRights.serialize(buffer, true) serializeInt32(version, buffer: buffer, boxed: false) break - case .updateChatPinnedMessage(let chatId, let id, let version): - if boxed { - buffer.appendInt32(-519195831) - } - serializeInt32(chatId, buffer: buffer, boxed: false) - serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - break case .updateFolderPeers(let folderPeers, let pts, let ptsCount): if boxed { buffer.appendInt32(422972864) @@ -6916,6 +6900,20 @@ public extension Api { serializeInt32(userId, buffer: buffer, boxed: false) action.serialize(buffer, true) break + case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount): + if boxed { + buffer.appendInt32(-309990731) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + break case .updatePinnedChannelMessages(let flags, let channelId, let messages, let pts, let ptsCount): if boxed { buffer.appendInt32(-2054649973) @@ -7051,14 +7049,10 @@ public extension Api { return ("updateDialogUnreadMark", [("flags", flags), ("peer", peer)]) case .updateLangPackTooLong(let langCode): return ("updateLangPackTooLong", [("langCode", langCode)]) - case .updateUserPinnedMessage(let userId, let id): - return ("updateUserPinnedMessage", [("userId", userId), ("id", id)]) case .updateMessagePoll(let flags, let pollId, let poll, let results): return ("updateMessagePoll", [("flags", flags), ("pollId", pollId), ("poll", poll), ("results", results)]) case .updateChatDefaultBannedRights(let peer, let defaultBannedRights, let version): return ("updateChatDefaultBannedRights", [("peer", peer), ("defaultBannedRights", defaultBannedRights), ("version", version)]) - case .updateChatPinnedMessage(let chatId, let id, let version): - return ("updateChatPinnedMessage", [("chatId", chatId), ("id", id), ("version", version)]) case .updateFolderPeers(let folderPeers, let pts, let ptsCount): return ("updateFolderPeers", [("folderPeers", folderPeers), ("pts", pts), ("ptsCount", ptsCount)]) case .updateDialogPinned(let flags, let folderId, let peer): @@ -7105,6 +7099,8 @@ public extension Api { return ("updatePeerBlocked", [("peerId", peerId), ("blocked", blocked)]) case .updateChannelUserTyping(let flags, let channelId, let topMsgId, let userId, let action): return ("updateChannelUserTyping", [("flags", flags), ("channelId", channelId), ("topMsgId", topMsgId), ("userId", userId), ("action", action)]) + case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount): + return ("updatePinnedMessages", [("flags", flags), ("peer", peer), ("messages", messages), ("pts", pts), ("ptsCount", ptsCount)]) case .updatePinnedChannelMessages(let flags, let channelId, let messages, let pts, let ptsCount): return ("updatePinnedChannelMessages", [("flags", flags), ("channelId", channelId), ("messages", messages), ("pts", pts), ("ptsCount", ptsCount)]) } @@ -8070,20 +8066,6 @@ public extension Api { return nil } } - public static func parse_updateUserPinnedMessage(_ reader: BufferReader) -> Update? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Update.updateUserPinnedMessage(userId: _1!, id: _2!) - } - else { - return nil - } - } public static func parse_updateMessagePoll(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -8129,23 +8111,6 @@ public extension Api { return nil } } - public static func parse_updateChatPinnedMessage(_ reader: BufferReader) -> Update? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.Update.updateChatPinnedMessage(chatId: _1!, id: _2!, version: _3!) - } - else { - return nil - } - } public static func parse_updateFolderPeers(_ reader: BufferReader) -> Update? { var _1: [Api.FolderPeer]? if let _ = reader.readInt32() { @@ -8558,6 +8523,33 @@ public extension Api { return nil } } + public static func parse_updatePinnedMessages(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: [Int32]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updatePinnedMessages(flags: _1!, peer: _2!, messages: _3!, pts: _4!, ptsCount: _5!) + } + else { + return nil + } + } public static func parse_updatePinnedChannelMessages(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index fd260485c3..62133159be 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -873,6 +873,11 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo updatedState.updateMinAvailableMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minId)) case let .updateDeleteMessages(messages, _, _): updatedState.deleteMessagesWithGlobalIds(messages) + case let .updatePinnedMessages(flags, peer, messages, _, _): + let peerId = peer.peerId + updatedState.updateMessagesPinned(ids: messages.map { id in + MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id) + }, pinned: (flags & (1 << 0)) != 0) case let .updateEditMessage(apiMessage, _, _): if let message = StoreMessage(apiMessage: apiMessage), case let .Id(messageId) = message.id { if let preCachedResources = apiMessage.preCachedResources { @@ -1139,28 +1144,6 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo channelsToPoll.insert(peerId) } } - case let .updateUserPinnedMessage(userId, id): - let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - updatedState.updateCachedPeerData(userPeerId, { current in - let previous: CachedUserData - if let current = current as? CachedUserData { - previous = current - } else { - previous = CachedUserData() - } - return previous.withUpdatedPinnedMessageId(id == 0 ? nil : MessageId(peerId: userPeerId, namespace: Namespaces.Message.Cloud, id: id)) - }) - case let .updateChatPinnedMessage(groupId, id, _): - let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: groupId) - updatedState.updateCachedPeerData(groupPeerId, { current in - let previous: CachedGroupData - if let current = current as? CachedGroupData { - previous = current - } else { - previous = CachedGroupData() - } - return previous.withUpdatedPinnedMessageId(id == 0 ? nil : MessageId(peerId: groupPeerId, namespace: Namespaces.Message.Cloud, id: id)) - }) case let .updatePeerBlocked(peerId, blocked): let userPeerId = peerId.peerId updatedState.updateCachedPeerData(userPeerId, { current in diff --git a/submodules/TelegramCore/Sources/UpdateGroup.swift b/submodules/TelegramCore/Sources/UpdateGroup.swift index 8145e9d2e9..8ae8b4c168 100644 --- a/submodules/TelegramCore/Sources/UpdateGroup.swift +++ b/submodules/TelegramCore/Sources/UpdateGroup.swift @@ -80,6 +80,8 @@ func apiUpdatePtsRange(_ update: Api.Update) -> (Int32, Int32)? { } else { return nil } + case let .updatePinnedMessages(_, _, _, pts, ptsCount): + return (pts, ptsCount) default: return nil } diff --git a/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift b/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift index a2a6b6bb1d..baa8233d92 100644 --- a/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift +++ b/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift @@ -25,185 +25,83 @@ public func requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: guard let peer = peer, let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } - if let channel = peer as? TelegramChannel, let inputPeer = apiInputPeer(channel) { + + if let channel = peer as? TelegramChannel { let canManagePin = channel.hasPermission(.pinMessages) + if !canManagePin { + return .fail(.generic) + } + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator, .admin: + break + default: + if let defaultBannedRights = group.defaultBannedRights { + if defaultBannedRights.flags.contains(.banPinMessages) { + return .fail(.generic) + } + } + } + } else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData { + if !cachedPeerData.canPinMessages { + return .fail(.generic) + } + } - if canManagePin { - var flags: Int32 = 0 - let messageId: Int32 - switch update { - case let .pin(id, silent): - messageId = id.id - if silent { - flags |= (1 << 0) - } - case let .clear(id): - messageId = id.id - flags |= 1 << 1 - } - - let request = Api.functions.messages.updatePinnedMessage(flags: flags, peer: inputPeer, id: messageId) - - return account.network.request(request) - |> mapError { _ -> UpdatePinnedMessageError in - return .generic - } - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - return account.postbox.transaction { transaction in - switch updates { - case let .updates(updates, _, _, _, _): - if updates.isEmpty { - if peerId.namespace == Namespaces.Peer.CloudChannel { - let messageId: MessageId - switch update { - case let .pin(id, _): - messageId = id - case let .clear(id): - messageId = id - } - transaction.updateMessage(messageId, update: { currentMessage in - let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) - var updatedTags = currentMessage.tags - switch update { - case .pin: - updatedTags.insert(.pinned) - case .clear: - updatedTags.remove(.pinned) - } - if updatedTags == currentMessage.tags { - return .skip - } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media)) - }) - } - } - default: - break - } - /*transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in - if let current = current as? CachedChannelData { - let pinnedMessageId: MessageId? - switch update { - case let .pin(id, _): - pinnedMessageId = id - case .clear: - pinnedMessageId = nil - } - return current.withUpdatedPinnedMessageId(pinnedMessageId) - } else { - return current - } - })*/ - } - |> mapError { _ -> UpdatePinnedMessageError in - } - } - } else { - return .fail(.generic) + var flags: Int32 = 0 + let messageId: Int32 + switch update { + case let .pin(id, silent): + messageId = id.id + if silent { + flags |= (1 << 0) } - } else { - var canPin = false - if let group = peer as? TelegramGroup { - switch group.role { - case .creator, .admin: - canPin = true - default: - if let defaultBannedRights = group.defaultBannedRights { - canPin = !defaultBannedRights.flags.contains(.banPinMessages) - } else { - canPin = true + case let .clear(id): + messageId = id.id + flags |= 1 << 1 + } + + let request = Api.functions.messages.updatePinnedMessage(flags: flags, peer: inputPeer, id: messageId) + + return account.network.request(request) + |> mapError { _ -> UpdatePinnedMessageError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + return account.postbox.transaction { transaction in + switch updates { + case let .updates(updates, _, _, _, _): + if updates.isEmpty { + if peerId.namespace == Namespaces.Peer.CloudChannel { + let messageId: MessageId + switch update { + case let .pin(id, _): + messageId = id + case let .clear(id): + messageId = id + } + transaction.updateMessage(messageId, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var updatedTags = currentMessage.tags + switch update { + case .pin: + updatedTags.insert(.pinned) + case .clear: + updatedTags.remove(.pinned) + } + if updatedTags == currentMessage.tags { + return .skip + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media)) + }) } + } + default: + break } - } else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData { - canPin = cachedPeerData.canPinMessages } - if canPin { - var flags: Int32 = 0 - let messageId: Int32 - switch update { - case let .pin(id, silent): - messageId = id.id - if silent { - flags |= (1 << 0) - } - case let .clear(id): - messageId = id.id - flags |= 1 << 1 - } - - let request = Api.functions.messages.updatePinnedMessage(flags: flags, peer: inputPeer, id: messageId) - - return account.network.request(request) - |> mapError { _ -> UpdatePinnedMessageError in - return .generic - } - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - return account.postbox.transaction { transaction in - switch updates { - case let .updates(updates, _, _, _, _): - if updates.isEmpty { - if peerId.namespace == Namespaces.Peer.CloudChannel { - let messageId: MessageId - switch update { - case let .pin(id, _): - messageId = id - case let .clear(id): - messageId = id - } - transaction.updateMessage(messageId, update: { currentMessage in - let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) - var updatedTags = currentMessage.tags - switch update { - case .pin: - updatedTags.insert(.pinned) - case .clear: - updatedTags.remove(.pinned) - } - if updatedTags == currentMessage.tags { - return .skip - } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media)) - }) - } - } - default: - break - } - - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in - if let _ = peer as? TelegramGroup { - let current = current as? CachedGroupData ?? CachedGroupData() - let pinnedMessageId: MessageId? - switch update { - case let .pin(id, _): - pinnedMessageId = id - case .clear: - pinnedMessageId = nil - } - return current.withUpdatedPinnedMessageId(pinnedMessageId) - } else if let _ = peer as? TelegramUser { - let current = current as? CachedUserData ?? CachedUserData() - - let pinnedMessageId: MessageId? - switch update { - case let .pin(id, _): - pinnedMessageId = id - case .clear: - pinnedMessageId = nil - } - return current.withUpdatedPinnedMessageId(pinnedMessageId) - } else { - return current - } - }) - } - |> mapError { _ -> UpdatePinnedMessageError in - } - } - } else { - return .fail(.generic) + |> mapError { _ -> UpdatePinnedMessageError in } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c700d4616b..69c21fd979 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3242,7 +3242,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func topPinnedMessageSignal(latest: Bool) -> Signal { let topPinnedMessage: Signal switch self.chatLocation { - case let .peer(peerId) where peerId.namespace == Namespaces.Peer.CloudChannel: + case let .peer(peerId): let replyHistory: Signal = (chatHistoryViewForLocation(ChatHistoryLocationInput(content: .Initial(count: 100), id: 0), context: self.context, chatLocation: .peer(peerId), chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, additionalData: []) |> castError(Bool.self) |> mapToSignal { update -> Signal in @@ -3554,16 +3554,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } case let .peer(peerId): - if peerId.namespace == Namespaces.Peer.CloudChannel { - pinnedMessageId = topPinnedMessage?.message.id - pinnedMessage = topPinnedMessage - } else { - if let pinnedMessageId = pinnedMessageId { - if let message = messages?[pinnedMessageId] { - pinnedMessage = ChatPinnedMessage(message: message, topMessageId: message.id) - } - } - } + pinnedMessageId = topPinnedMessage?.message.id + pinnedMessage = topPinnedMessage } var pinnedMessageUpdated = false