diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5fa6325af7..df99dbe54a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8242,5 +8242,8 @@ Sorry for the inconvenience."; "Undo.DeletedTopic" = "Topic Deleted"; +"ChatList.MaxThreadPinsFinalText_1" = "Sorry, you can't pin more than **%@** thread to the top. Unpin some that are currently pinned."; +"ChatList.MaxThreadPinsFinalText_any" = "Sorry, you can't pin more than **%@** threads to the top. Unpin some that are currently pinned."; + "EmojiSearch.SearchTopicIconsPlaceholder" = "Search Topic Icons"; "EmojiSearch.SearchTopicIconsEmptyResult" = "No emoji found"; diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index f53523f584..8d0fbc5c01 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -519,7 +519,19 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: items.append(.action(ContextMenuActionItem(text: isPinned ? presentationData.strings.ChatList_Context_Unpin : presentationData.strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = context.engine.peers.setForumChannelTopicPinned(id: peerId, threadId: threadId, isPinned: !isPinned).start() + let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId) + |> deliverOnMainQueue).start(error: { error in + switch error { + case let .limitReached(count): + if let chatListController = chatListController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text = presentationData.strings.ChatList_MaxThreadPinsFinalText(Int32(count)) + chatListController.present(textAlertController(context: context, title: presentationData.strings.Premium_LimitReached, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true), in: .window(.root)) + } + default: + break + } + }) }))) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index df1c9737ad..f1dea07f7e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -3874,7 +3874,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func setPeerThreadPinned(peerId: EnginePeer.Id, threadId: Int64, isPinned: Bool) { - self.actionDisposables.add(self.context.engine.peers.setForumChannelTopicPinned(id: peerId, threadId: threadId, isPinned: isPinned).start()) + self.actionDisposables.add(self.context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId).start()) } public func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool = false, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index ccc802aefe..d24ec3f57e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -795,7 +795,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage } - let (_, initialHideAuthor, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) + let (_, initialHideAuthor, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, case .user = author { result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).string)" } @@ -829,7 +829,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage } - let (_, initialHideAuthor, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) + let (_, initialHideAuthor, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, case .user = author { result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).string)" } @@ -1387,7 +1387,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var hideAuthor = false switch contentPeer { case let .chat(itemPeer): - var (peer, initialHideAuthor, messageText, spoilers, customEmojiRanges) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup) + var (peer, initialHideAuthor, messageText, spoilers, customEmojiRanges) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup) if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText { initialHideAuthor = true diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 1048701c8a..b4a7b82ea0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -45,11 +45,18 @@ private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType { return currentType } -public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) { +public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, contentSettings: ContentSettings, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) { let peer: EnginePeer? let message = messages.last + if let restrictionReason = message?._asMessage().restrictionReason(platform: "ios", contentSettings: contentSettings) { + return (nil, false, restrictionReason, nil, nil) + } + if let restrictionReason = chatPeer.chatMainPeer?.restrictionText(platform: "ios", contentSettings: contentSettings) { + return (nil, false, restrictionReason, nil, nil) + } + var hideAuthor = false var messageText: String var spoilers: [NSRange]? diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 1438dcf409..0ab3d0eb17 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1697,85 +1697,160 @@ public final class ChatListNode: ListView { }) self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in - if let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries { - guard case let .chatList(groupId) = strongSelf.location else { - return .single(false) + guard let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries else { + return .single(false) + } + guard fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count else { + return .single(false) + } + + switch strongSelf.location { + case let .chatList(groupId): + let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] + let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] + + var referenceId: EngineChatList.PinnedItem.Id? + var beforeAll = false + switch toEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _): + if promoInfo != nil { + beforeAll = true + } else { + if case let .chatList(chatListIndex) = index { + referenceId = .peer(chatListIndex.messageIndex.id.peerId) + } + } + default: + break } - if fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count { - let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] - let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] + if case let .index(index) = fromEntry.sortIndex, case let .chatList(chatListIndex) = index, let _ = chatListIndex.pinningIndex { + let location: TogglePeerChatPinnedLocation + if let chatListFilter = chatListFilter { + location = .filter(chatListFilter.id) + } else { + location = .group(groupId._asGroup()) + } - var referenceId: EngineChatList.PinnedItem.Id? - var beforeAll = false - switch toEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _): - if promoInfo != nil { - beforeAll = true - } else { - if case let .chatList(chatListIndex) = index { - referenceId = .peer(chatListIndex.messageIndex.id.peerId) + let engine = strongSelf.context.engine + return engine.peers.getPinnedItemIds(location: location) + |> mapToSignal { itemIds -> Signal in + var itemIds = itemIds + + var itemId: EngineChatList.PinnedItem.Id? + switch fromEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + if case let .chatList(index) = index { + itemId = .peer(index.messageIndex.id.peerId) } - } default: break - } - - if case let .index(index) = fromEntry.sortIndex, case let .chatList(chatListIndex) = index, let _ = chatListIndex.pinningIndex { - let location: TogglePeerChatPinnedLocation - if let chatListFilter = chatListFilter { - location = .filter(chatListFilter.id) - } else { - location = .group(groupId._asGroup()) } - - let engine = strongSelf.context.engine - return engine.peers.getPinnedItemIds(location: location) - |> mapToSignal { itemIds -> Signal in - var itemIds = itemIds - - var itemId: EngineChatList.PinnedItem.Id? - switch fromEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): - if case let .chatList(index) = index { - itemId = .peer(index.messageIndex.id.peerId) - } - default: - break - } - - if let itemId = itemId { - itemIds = itemIds.filter({ $0 != itemId }) - if let referenceId = referenceId { - var inserted = false - for i in 0 ..< itemIds.count { - if itemIds[i] == referenceId { - if fromIndex < toIndex { - itemIds.insert(itemId, at: i + 1) - } else { - itemIds.insert(itemId, at: i) - } - inserted = true - break + + if let itemId = itemId { + itemIds = itemIds.filter({ $0 != itemId }) + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< itemIds.count { + if itemIds[i] == referenceId { + if fromIndex < toIndex { + itemIds.insert(itemId, at: i + 1) + } else { + itemIds.insert(itemId, at: i) } + inserted = true + break } - if !inserted { - itemIds.append(itemId) - } - } else if beforeAll { - itemIds.insert(itemId, at: 0) - } else { + } + if !inserted { itemIds.append(itemId) } - return engine.peers.reorderPinnedItemIds(location: location, itemIds: itemIds) + } else if beforeAll { + itemIds.insert(itemId, at: 0) } else { - return .single(false) + itemIds.append(itemId) } + return engine.peers.reorderPinnedItemIds(location: location, itemIds: itemIds) + } else { + return .single(false) } } + } else { + return .single(false) + } + case let .forum(peerId): + let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] + let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] + + var referenceId: Int64? + var beforeAll = false + switch toEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _): + if promoInfo != nil { + beforeAll = true + } else { + if case let .forum(_, _, threadId, _, _) = index { + referenceId = threadId + } + } + default: + break + } + + if case let .index(index) = fromEntry.sortIndex, case let .forum(pinningIndex, _, _, _, _) = index, case .index = pinningIndex { + let engine = strongSelf.context.engine + return engine.peers.getForumChannelPinnedTopics(id: peerId) + |> mapToSignal { itemIds -> Signal in + var itemIds = itemIds + + var itemId: Int64? + switch fromEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + if case let .forum(_, _, threadId, _, _) = index { + itemId = threadId + } + default: + break + } + + if let itemId = itemId { + itemIds = itemIds.filter({ $0 != itemId }) + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< itemIds.count { + if itemIds[i] == referenceId { + if fromIndex < toIndex { + itemIds.insert(itemId, at: i + 1) + } else { + itemIds.insert(itemId, at: i) + } + inserted = true + break + } + } + if !inserted { + itemIds.append(itemId) + } + } else if beforeAll { + itemIds.insert(itemId, at: 0) + } else { + itemIds.append(itemId) + } + return engine.peers.setForumChannelPinnedTopics(id: peerId, threadIds: itemIds) + |> map { _ -> Bool in + } + |> `catch` { _ -> Signal in + return .single(false) + } + |> then(Signal.single(true)) + } else { + return .single(false) + } + } + } else { + return .single(false) } } - return .single(false) } var startedScrollingAtUpperBound = false diff --git a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift index b3ed30e88a..ca8c03185b 100644 --- a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift +++ b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift @@ -4,10 +4,16 @@ import Postbox public extension Message { func isRestricted(platform: String, contentSettings: ContentSettings) -> Bool { + return self.restrictionReason(platform: platform, contentSettings: contentSettings) != nil + } + + func restrictionReason(platform: String, contentSettings: ContentSettings) -> String? { if let attribute = self.restrictedContentAttribute { - return attribute.platformText(platform: platform, contentSettings: contentSettings) != nil + if let value = attribute.platformText(platform: platform, contentSettings: contentSettings) { + return value + } } - return false + return nil } } diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index 73a86ae8f0..1a52c171fe 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -268,7 +268,7 @@ private final class ChatListViewSpaceState { } postboxLog("allIndices not unique, repeated: \(debugRepeatedIndices)") - assert(false) + //assert(false) //preconditionFailure() } if Set(allEntityIds).count != allEntityIds.count { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 3b335113a1..a08bc82d73 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -778,7 +778,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-761649164] = { return Api.Update.parse_updateChannelMessageForwards($0) } dict[-232346616] = { return Api.Update.parse_updateChannelMessageViews($0) } dict[-1738720581] = { return Api.Update.parse_updateChannelParticipant($0) } - dict[-158027602] = { return Api.Update.parse_updateChannelPinnedTopic($0) } + dict[422509539] = { return Api.Update.parse_updateChannelPinnedTopic($0) } + dict[-31881726] = { return Api.Update.parse_updateChannelPinnedTopics($0) } dict[-366410403] = { return Api.Update.parse_updateChannelReadMessagesContents($0) } dict[277713951] = { return Api.Update.parse_updateChannelTooLong($0) } dict[-1937192669] = { return Api.Update.parse_updateChannelUserTyping($0) } diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 5efcea25c9..9d016b2a92 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -671,7 +671,8 @@ public extension Api { case updateChannelMessageForwards(channelId: Int64, id: Int32, forwards: Int32) case updateChannelMessageViews(channelId: Int64, id: Int32, views: Int32) case updateChannelParticipant(flags: Int32, channelId: Int64, date: Int32, actorId: Int64, userId: Int64, prevParticipant: Api.ChannelParticipant?, newParticipant: Api.ChannelParticipant?, invite: Api.ExportedChatInvite?, qts: Int32) - case updateChannelPinnedTopic(flags: Int32, channelId: Int64, topicId: Int32?) + case updateChannelPinnedTopic(flags: Int32, channelId: Int64, topicId: Int32) + case updateChannelPinnedTopics(flags: Int32, channelId: Int64, order: [Int32]?) case updateChannelReadMessagesContents(flags: Int32, channelId: Int64, topMsgId: Int32?, messages: [Int32]) case updateChannelTooLong(flags: Int32, channelId: Int64, pts: Int32?) case updateChannelUserTyping(flags: Int32, channelId: Int64, topMsgId: Int32?, fromId: Api.Peer, action: Api.SendMessageAction) @@ -927,11 +928,23 @@ public extension Api { break case .updateChannelPinnedTopic(let flags, let channelId, let topicId): if boxed { - buffer.appendInt32(-158027602) + buffer.appendInt32(422509539) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(channelId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topicId!, buffer: buffer, boxed: false)} + serializeInt32(topicId, buffer: buffer, boxed: false) + break + case .updateChannelPinnedTopics(let flags, let channelId, let order): + if boxed { + buffer.appendInt32(-31881726) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(channelId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order!.count)) + for item in order! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break case .updateChannelReadMessagesContents(let flags, let channelId, let topMsgId, let messages): if boxed { @@ -1741,6 +1754,8 @@ public extension Api { return ("updateChannelParticipant", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("date", String(describing: date)), ("actorId", String(describing: actorId)), ("userId", String(describing: userId)), ("prevParticipant", String(describing: prevParticipant)), ("newParticipant", String(describing: newParticipant)), ("invite", String(describing: invite)), ("qts", String(describing: qts))]) case .updateChannelPinnedTopic(let flags, let channelId, let topicId): return ("updateChannelPinnedTopic", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topicId", String(describing: topicId))]) + case .updateChannelPinnedTopics(let flags, let channelId, let order): + return ("updateChannelPinnedTopics", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("order", String(describing: order))]) case .updateChannelReadMessagesContents(let flags, let channelId, let topMsgId, let messages): return ("updateChannelReadMessagesContents", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topMsgId", String(describing: topMsgId)), ("messages", String(describing: messages))]) case .updateChannelTooLong(let flags, let channelId, let pts): @@ -2307,12 +2322,31 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateChannelPinnedTopic(flags: _1!, channelId: _2!, topicId: _3!) + } + else { + return nil + } + } + public static func parse_updateChannelPinnedTopics(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { - return Api.Update.updateChannelPinnedTopic(flags: _1!, channelId: _2!, topicId: _3) + return Api.Update.updateChannelPinnedTopics(flags: _1!, channelId: _2!, order: _3) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index dbfb3fc069..1ba4add6d3 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -2443,6 +2443,27 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func reorderPinnedForumTopics(flags: Int32, channel: Api.InputChannel, order: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(693150095) + serializeInt32(flags, buffer: buffer, boxed: false) + channel.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order.count)) + for item in order { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "channels.reorderPinnedForumTopics", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.channels { static func reorderUsernames(channel: Api.InputChannel, order: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 2b3a8c16a3..de023e5d59 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -179,6 +179,8 @@ public class ManagedAudioSessionControl { } public final class ManagedAudioSession { + public private(set) static var shared: ManagedAudioSession? + private var nextId: Int32 = 0 private let queue: Queue private let hasLoudspeaker: Bool @@ -256,6 +258,8 @@ public final class ManagedAudioSession { self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn() self.updateCurrentAudioRouteInfo() } + + ManagedAudioSession.shared = self } deinit { @@ -784,6 +788,61 @@ public final class ManagedAudioSession { } } + public func applyVoiceChatOutputModeInCurrentAudioSession(outputMode: AudioSessionOutputMode) { + managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession \(outputMode)") + + do { + var resetToBuiltin = false + switch outputMode { + case .system: + resetToBuiltin = true + case let .custom(output): + switch output { + case .builtin: + resetToBuiltin = true + case .speaker: + if let routes = AVAudioSession.sharedInstance().availableInputs { + for route in routes { + if route.portType == .builtInMic { + let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) + break + } + } + } + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + case .headphones: + break + case let .port(port): + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + if let routes = AVAudioSession.sharedInstance().availableInputs { + for route in routes { + if route.uid == port.uid { + let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) + break + } + } + } + } + case .speakerIfNoHeadphones: + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + } + + if resetToBuiltin { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + if let routes = AVAudioSession.sharedInstance().availableInputs { + for route in routes { + if route.portType == .builtInMic { + let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) + break + } + } + } + } + } catch let e { + managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession error: \(e)") + } + } + private func setupOutputMode(_ outputMode: AudioSessionOutputMode, type: ManagedAudioSessionType) throws { managedAudioSessionLog("ManagedAudioSession setup \(outputMode) for \(type)") var resetToBuiltin = false diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 3629693c2b..7fdfee4ea1 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -8,6 +8,7 @@ import TelegramCore import SwiftSignalKit import AppBundle import AccountContext +import TelegramAudio private let sharedProviderDelegate: AnyObject? = { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { @@ -107,6 +108,10 @@ public final class CallKitIntegration { } } } + + public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { + (sharedProviderDelegate as? CallKitProviderDelegate)?.applyVoiceChatOutputMode(outputMode: outputMode) + } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) @@ -125,6 +130,9 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { private var setCallMuted: ((UUID, Bool) -> Void)? private var audioSessionActivationChanged: ((Bool) -> Void)? + private var isAudioSessionActive: Bool = false + private var pendingVoiceChatOutputMode: AudioSessionOutputMode? + private let disposableSet = DisposableSet() fileprivate var audioSessionActivePromise: ValuePromise? @@ -163,7 +171,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { private func requestTransaction(_ transaction: CXTransaction, completion: ((Bool) -> Void)? = nil) { self.callController.request(transaction) { error in if let error = error { - print("Error requesting transaction: \(error)") + print("Error requesting transaction \(transaction): \(error)") } completion?(error == nil) } @@ -238,6 +246,12 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { update.supportsDTMF = false update.hasVideo = isVideo + do { + try AVAudioSession.sharedInstance().setMode(.default) + } catch let e { + print("AVAudioSession.sharedInstance().setMode(.default) error \(e)") + } + self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in completion?(error as NSError?) }) @@ -321,13 +335,28 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + print("provider didActivate default? \(audioSession === AVAudioSession.sharedInstance())") + self.isAudioSessionActive = true self.audioSessionActivationChanged?(true) self.audioSessionActivePromise?.set(true) + + if let outputMode = self.pendingVoiceChatOutputMode { + self.pendingVoiceChatOutputMode = nil + ManagedAudioSession.shared?.applyVoiceChatOutputModeInCurrentAudioSession(outputMode: outputMode) + } } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + self.isAudioSessionActive = false self.audioSessionActivationChanged?(false) self.audioSessionActivePromise?.set(false) } + + func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { + if self.isAudioSessionActive { + ManagedAudioSession.shared?.applyVoiceChatOutputModeInCurrentAudioSession(outputMode: outputMode) + } else { + self.pendingVoiceChatOutputMode = outputMode + } + } } - diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index e877e21ebb..6cf0a084d5 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -413,6 +413,9 @@ public final class PresentationCallImpl: PresentationCall { return } strongSelf.audioOutputStateValue = (availableOutputs, currentOutput) + if let currentOutput = currentOutput { + strongSelf.currentAudioOutputValue = currentOutput + } var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) if !didReceiveAudioOutputs { @@ -437,7 +440,7 @@ public final class PresentationCallImpl: PresentationCall { let audioSessionActive: Signal if let callKitIntegration = strongSelf.callKitIntegration { audioSessionActive = callKitIntegration.audioSessionActive - |> filter { $0 } + /*|> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in if let strongSelf = self, let _ = strongSelf.audioSessionControl { //audioSessionControl.activate({ _ in }) @@ -445,7 +448,7 @@ public final class PresentationCallImpl: PresentationCall { subscriber.putNext(true) subscriber.putCompletion() return EmptyDisposable - }) + })*/ } else { audioSessionControl.activate({ _ in }) audioSessionActive = .single(true) @@ -534,8 +537,12 @@ public final class PresentationCallImpl: PresentationCall { } if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil { - audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) - audioSessionControl.setup(synchronous: true) + if let callKitIntegration = self.callKitIntegration { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } else { + audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) + audioSessionControl.setup(synchronous: true) + } } let mappedVideoState: PresentationCallState.VideoState @@ -1031,7 +1038,11 @@ public final class PresentationCallImpl: PresentationCall { )) if let audioSessionControl = self.audioSessionControl { - audioSessionControl.setOutputMode(.custom(output)) + if let callKitIntegration = self.callKitIntegration { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } else { + audioSessionControl.setOutputMode(.custom(output)) + } } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 369a610dff..51ef7b5d83 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1648,7 +1648,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { outgoingAudioBitrateKbit = Int32(value) } - genericCallContext = .call(OngoingGroupCallContext(video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in + genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: self.audioSessionActive.get(), video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in let disposable = MetaDisposable() Queue.mainQueue().async { guard let strongSelf = self else { @@ -2966,7 +2966,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.hasScreencast = true - let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false, logPath: "") + let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false, logPath: "") self.screencastCallContext = screencastCallContext self.screencastJoinDisposable.set((screencastCallContext.joinPayload diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index adb0f1a6dc..950622bcb8 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -93,7 +93,8 @@ enum AccountStateMutationOperation { case ReadSecretOutbox(peerId: PeerId, maxTimestamp: Int32, actionTimestamp: Int32) case AddPeerInputActivity(chatPeerId: PeerActivitySpace, peerId: PeerId?, activity: PeerInputActivity?) case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation) - case UpdatePinnedTopic(peerId: PeerId, threadId: Int64?) + case UpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool) + case UpdatePinnedTopicOrder(peerId: PeerId, threadIds: [Int64]) case ReadMessageContents((PeerId?, [Int32])) case UpdateMessageImpressionCount(MessageId, Int32) case UpdateMessageForwardsCount(MessageId, Int32) @@ -475,8 +476,12 @@ struct AccountMutableState { self.addOperation(.UpdatePinnedItemIds(groupId, operation)) } - mutating func addUpdatePinnedTopic(peerId: PeerId, threadId: Int64?) { - self.addOperation(.UpdatePinnedTopic(peerId: peerId, threadId: threadId)) + mutating func addUpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool) { + self.addOperation(.UpdatePinnedTopic(peerId: peerId, threadId: threadId, isPinned: isPinned)) + } + + mutating func addUpdatePinnedTopicOrder(peerId: PeerId, threadIds: [Int64]) { + self.addOperation(.UpdatePinnedTopicOrder(peerId: peerId, threadIds: threadIds)) } mutating func addReadMessagesContents(_ peerIdsAndMessageIds: (PeerId?, [Int32])) { @@ -545,7 +550,7 @@ struct AccountMutableState { mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 21bf65483d..4b4fa8e79d 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -367,21 +367,16 @@ func _internal_setForumChannelTopicClosed(account: Account, id: EnginePeer.Id, t public enum SetForumChannelTopicPinnedError { case generic + case limitReached(Int) } -func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, threadId: Int64, isPinned: Bool) -> Signal { +func _internal_setForumChannelPinnedTopics(account: Account, id: EnginePeer.Id, threadIds: [Int64]) -> Signal { return account.postbox.transaction { transaction -> Api.InputChannel? in guard let inputChannel = transaction.getPeer(id).flatMap(apiInputChannel) else { return nil } - if isPinned { - transaction.setPeerPinnedThreads(peerId: id, threadIds: [threadId]) - } else { - if transaction.getPeerPinnedThreads(peerId: id).contains(threadId) { - transaction.setPeerPinnedThreads(peerId: id, threadIds: []) - } - } + transaction.setPeerPinnedThreads(peerId: id, threadIds: threadIds) return inputChannel } @@ -390,13 +385,11 @@ func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, t guard let inputChannel = inputChannel else { return .fail(.generic) } - var flags: Int32 = 0 - flags |= (1 << 2) - return account.network.request(Api.functions.channels.updatePinnedForumTopic( + return account.network.request(Api.functions.channels.reorderPinnedForumTopics( + flags: 1 << 0, channel: inputChannel, - topicId: Int32(clamping: threadId), - pinned: isPinned ? .boolTrue : .boolFalse + order: threadIds.map(Int32.init(clamping:)) )) |> mapError { _ -> SetForumChannelTopicPinnedError in return .generic diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 96da863315..e8538fcfb9 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -114,6 +114,11 @@ private func peerIdsRequiringLocalChatStateFromUpdates(_ updates: [Api.Update]) case let .updateChannelTooLong(_, channelId, _): let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) peerIds.insert(peerId) + case let .updateChannelPinnedTopics(_, channelId, order): + if order == nil { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + peerIds.insert(peerId) + } case let .updateFolderPeers(folderPeers, _, _): for peer in folderPeers { switch peer { @@ -337,11 +342,16 @@ private func peerIdsRequiringLocalChatStateFromDifference(_ difference: Api.upda peerIds.insert(messageId.peerId) } switch update { - case let .updateChannelTooLong(_, channelId, _): + case let .updateChannelTooLong(_, channelId, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + peerIds.insert(peerId) + case let .updateChannelPinnedTopics(_, channelId, order): + if order == nil { let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) peerIds.insert(peerId) - default: - break + } + default: + break } } case .differenceEmpty: @@ -359,11 +369,16 @@ private func peerIdsRequiringLocalChatStateFromDifference(_ difference: Api.upda peerIds.insert(messageId.peerId) } switch update { - case let .updateChannelTooLong(_, channelId, _): + case let .updateChannelTooLong(_, channelId, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + peerIds.insert(peerId) + case let .updateChannelPinnedTopics(_, channelId, order): + if order == nil { let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) peerIds.insert(peerId) - default: - break + } + default: + break } } case .differenceTooLong: @@ -725,6 +740,20 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { } else { updatesByChannel[peerId]!.append(update) } + case let .updateChannelPinnedTopic(_, channelId, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + if updatesByChannel[peerId] == nil { + updatesByChannel[peerId] = [update] + } else { + updatesByChannel[peerId]!.append(update) + } + case let .updateChannelPinnedTopics(_, channelId, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + if updatesByChannel[peerId] == nil { + updatesByChannel[peerId] = [update] + } else { + updatesByChannel[peerId]!.append(update) + } case let .updateDeleteChannelMessages(channelId, _, _, _): let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) if updatesByChannel[peerId] == nil { @@ -860,6 +889,15 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: channelsToPoll.insert(peerId) } } + case let .updateChannelPinnedTopics(_, channelId, order): + if let order = order { + updatedState.addUpdatePinnedTopicOrder(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadIds: order.map(Int64.init)) + } else { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + if !channelsToPoll.contains(peerId) { + channelsToPoll.insert(peerId) + } + } case let .updateDeleteChannelMessages(channelId, messages, pts: pts, ptsCount): let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) if let previousState = updatedState.channelStates[peerId] { @@ -1375,8 +1413,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } else { updatedState.addUpdatePinnedItemIds(groupId: groupId, operation: .sync) } - case let .updateChannelPinnedTopic(_, channelId, topicId): - updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: topicId.flatMap(Int64.init)) + case let .updateChannelPinnedTopic(flags, channelId, topicId): + let isPinned = (flags & (1 << 0)) != 0 + updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned) case let .updateReadMessagesContents(messages, _, _): updatedState.addReadMessagesContents((nil, messages)) case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages): @@ -2847,7 +2886,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3757,12 +3796,18 @@ func replayFinalState( case .sync: addSynchronizePinnedChatsOperation(transaction: transaction, groupId: groupId) } - case let .UpdatePinnedTopic(peerId, threadId): - if let threadId = threadId { - transaction.setPeerPinnedThreads(peerId: peerId, threadIds: [threadId]) + case let .UpdatePinnedTopic(peerId, threadId, isPinned): + var currentThreadIds = transaction.getPeerPinnedThreads(peerId: peerId) + if isPinned { + if !currentThreadIds.contains(threadId) { + currentThreadIds.insert(threadId, at: 0) + } } else { - transaction.setPeerPinnedThreads(peerId: peerId, threadIds: []) + currentThreadIds.removeAll(where: { $0 == threadId }) } + transaction.setPeerPinnedThreads(peerId: peerId, threadIds: currentThreadIds) + case let .UpdatePinnedTopicOrder(peerId, threadIds): + transaction.setPeerPinnedThreads(peerId: peerId, threadIds: threadIds) case let .ReadMessageContents(peerIdAndMessageIds): let (peerId, messageIds) = peerIdAndMessageIds diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index ec2600126f..d1296ecee5 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 148 + return 149 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 4b1d5b6328..bc38a263fc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -858,8 +858,34 @@ public extension TelegramEngine { |> ignoreValues } - public func setForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64, isPinned: Bool) -> Signal { - return _internal_setForumChannelTopicPinned(account: self.account, id: id, threadId: threadId, isPinned: isPinned) + public func toggleForumChannelTopicPinned(id: EnginePeer.Id, threadId: Int64) -> Signal { + return self.account.postbox.transaction { transaction -> [Int64] in + return transaction.getPeerPinnedThreads(peerId: id) + } + |> castError(SetForumChannelTopicPinnedError.self) + |> mapToSignal { threadIds -> Signal in + var threadIds = threadIds + if threadIds.contains(threadId) { + threadIds.removeAll(where: { $0 == threadId }) + } else { + if threadIds.count + 1 > 5 { + return .fail(.limitReached(5)) + } + threadIds.insert(threadId, at: 0) + } + + return _internal_setForumChannelPinnedTopics(account: self.account, id: id, threadIds: threadIds) + } + } + + public func getForumChannelPinnedTopics(id: EnginePeer.Id) -> Signal<[Int64], NoError> { + return self.account.postbox.transaction { transcation -> [Int64] in + return transcation.getPeerPinnedThreads(peerId: id) + } + } + + public func setForumChannelPinnedTopics(id: EnginePeer.Id, threadIds: [Int64]) -> Signal { + return _internal_setForumChannelPinnedTopics(account: self.account, id: id, threadIds: threadIds) } public func forumChannelTopicNotificationExceptions(id: EnginePeer.Id) -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 39134577c0..47d99d047d 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -30,7 +30,7 @@ public extension Peer { if let restrictionInfo = restrictionInfo { for rule in restrictionInfo.rules { - if rule.platform == "all" || rule.platform == platform { + if rule.platform == "all" || rule.platform == platform || contentSettings.addContentRestrictionReasons.contains(rule.platform) { if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { return rule.text } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9ac9d6e617..47e9c30800 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -6132,12 +6132,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var buttonKeyboardMessage = combinedInitialData.buttonKeyboardMessage + if let buttonKeyboardMessageValue = buttonKeyboardMessage, buttonKeyboardMessageValue.isRestricted(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with({ $0 })) { + buttonKeyboardMessage = nil + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { updated in var updated = updated updated = updated.updatedInterfaceState({ _ in return interfaceState }) - updated = updated.updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) + updated = updated.updatedKeyboardButtonsMessage(buttonKeyboardMessage) updated = updated.updatedPinnedMessageId(pinnedMessageId) updated = updated.updatedPinnedMessage(pinnedMessage) updated = updated.updatedPeerIsBlocked(peerIsBlocked) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index fe404530e8..c5df790fc3 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1396,7 +1396,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { forceUpdateAll = true } - let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll) + var keyboardButtonsMessage = view.topTaggedMessages.first + if let keyboardButtonsMessageValue = keyboardButtonsMessage, keyboardButtonsMessageValue.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with({ $0 })) { + keyboardButtonsMessage = nil + } + + let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll) var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, transition: rawTransition) if disableAnimations { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index f3acc29e43..a74c380968 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -181,17 +181,6 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } - } else { - if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { - if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { - return (currentPanel, nil) - } else { - let panel = ChatRestrictedInputPanelNode() - panel.context = context - panel.interfaceInteraction = interfaceInteraction - return (panel, nil) - } - } } } @@ -248,6 +237,22 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } + + if channel.flags.contains(.isForum) { + if let _ = chatPresentationInterfaceState.threadData { + } else { + if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } + } + } } else if let group = peer as? TelegramGroup { switch group.membership { case .Removed, .Left: diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 3cdddc2b07..e08be944a7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1167,7 +1167,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { - } else if let threadId = item.message.threadId, Int64(replyAttribute.messageId.id) == threadId, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { } else { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 82fc983f3b..12714c5a4e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1418,7 +1418,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } else if let attribute = attribute as? ReplyMessageAttribute { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == attribute.messageId { - } else if let threadId = firstMessage.threadId, Int64(attribute.messageId.id) == threadId, let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { } else { replyMessage = firstMessage.associatedMessages[attribute.messageId] } diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 514c8ebdba..549e1668fa 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData { if let chatPeer = message.peers[item.message.id.peerId] { let authorName = message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - let (_, _, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId) + let (_, _, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId) var text = messageText diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index ac2d2e9950..171c10999c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -97,8 +97,6 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let previousMediaReference = maybeNode?.previousMediaReference return { arguments in - //presentationData, strings, context, type, message, parentMessage, constrainedSize - let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) let titleFont = Font.medium(fontSize) let textFont = Font.regular(fontSize) @@ -114,7 +112,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) + var (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) + + if let threadId = arguments.parentMessage.threadId, Int64(arguments.message.id.id) == threadId, let channel = arguments.parentMessage.peers[arguments.parentMessage.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), let threadInfo = arguments.parentMessage.associatedThreadInfo { + titleString = "\(threadInfo.title)" + textString = NSAttributedString() + } let placeholderColor: UIColor = arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 787fd8f8fb..d79c16eb07 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -611,7 +611,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { - } else if let threadId = item.message.threadId, Int64(replyAttribute.messageId.id) == threadId, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { } else { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 4092b160f3..a6da62508f 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -621,6 +621,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let components = URLComponents(string: "/?" + query) { var channelId: Int64? var postId: Int32? + var threadId: Int64? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -628,12 +629,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur channelId = Int64(value) } else if queryItem.name == "post" { postId = Int32(value) + } else if queryItem.name == "thread" { + threadId = Int64(value) } } } } - if let channelId = channelId, let postId = postId { - convertedUrl = "https://t.me/c/\(channelId)/\(postId)" + if let channelId = channelId { + if let postId = postId { + if let threadId = threadId { + convertedUrl = "https://t.me/c/\(channelId)/\(threadId)/\(postId)" + } else { + convertedUrl = "https://t.me/c/\(channelId)/\(postId)" + } + } else if let threadId = threadId { + convertedUrl = "https://t.me/c/\(channelId)/\(threadId)" + } } } } @@ -652,6 +663,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur var attach: String? var startAttach: String? var choose: String? + var threadId: Int64? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -677,6 +689,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur startAttach = value } else if queryItem.name == "choose" { choose = value + } else if queryItem.name == "thread" { + threadId = Int64(value) } } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { voiceChat = "" @@ -694,8 +708,15 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = "https://t.me/+\(phone)" } else if let domain = domain { var result = "https://t.me/\(domain)" - if let post = post, let postValue = Int(post) { - result += "/\(postValue)" + if let threadId = threadId { + result += "/\(threadId)" + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } + } else { + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } } if let start = start { result += "?start=\(start)" diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index cc912d35a3..1e47048fb3 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -416,7 +416,9 @@ public final class OngoingGroupCallContext { private let broadcastPartsSource = Atomic(value: nil) - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { + private let audioSessionActiveDisposable = MetaDisposable() + + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { self.queue = queue var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? @@ -571,9 +573,18 @@ public final class OngoingGroupCallContext { strongSelf.joinPayload.set(.single((payload, ssrc))) } }) + + self.audioSessionActiveDisposable.set((audioSessionActive + |> deliverOn(queue)).start(next: { [weak self] isActive in + guard let self else { + return + } + self.context.setManualAudioSessionIsActive(isActive) + })) } deinit { + self.audioSessionActiveDisposable.dispose() } func setJoinResponse(payload: String) { @@ -936,10 +947,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath) }) } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 8f24013ef5..776b3d71c2 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -326,6 +326,7 @@ private protocol OngoingCallThreadLocalContextProtocol: AnyObject { func nativeVersion() -> String func nativeGetDerivedState() -> Data func addExternalAudioData(data: Data) + func nativeSetIsAudioSessionActive(isActive: Bool) } private final class OngoingCallThreadLocalContextHolder { @@ -381,6 +382,9 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { func addExternalAudioData(data: Data) { } + + func nativeSetIsAudioSessionActive(isActive: Bool) { + } } public final class OngoingCallVideoCapturer { @@ -573,6 +577,10 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt func addExternalAudioData(data: Data) { self.addExternalAudioData(data) } + + func nativeSetIsAudioSessionActive(isActive: Bool) { + self.setManualAudioSessionIsActive(isActive) + } } private extension OngoingCallContextState.State { @@ -726,6 +734,7 @@ public final class OngoingCallContext { } private let audioSessionDisposable = MetaDisposable() + private let audioSessionActiveDisposable = MetaDisposable() private var networkTypeDisposable: Disposable? public static var maxLayer: Int32 { @@ -886,7 +895,7 @@ public final class OngoingCallContext { callSessionManager.sendSignalingData(internalId: internalId, data: data) } } - }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "") + }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "", useManualAudioSessionControl: true) strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) context.stateChanged = { [weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, _ in @@ -950,6 +959,16 @@ public final class OngoingCallContext { self?.audioLevelPromise.set(.single(level)) } + strongSelf.audioSessionActiveDisposable.set((audioSessionActive + |> deliverOn(queue)).start(next: { isActive in + guard let strongSelf = self else { + return + } + strongSelf.withContext { context in + context.nativeSetIsAudioSessionActive(isActive: isActive) + } + })) + strongSelf.networkTypeDisposable = (updatedNetworkType |> deliverOn(queue)).start(next: { networkType in self?.withContext { context in @@ -1010,6 +1029,7 @@ public final class OngoingCallContext { } self.audioSessionDisposable.dispose() + self.audioSessionActiveDisposable.dispose() self.networkTypeDisposable?.dispose() } @@ -1256,6 +1276,8 @@ private final class CallSignalingConnectionImpl: CallSignalingConnection { } func start() { + OngoingCallThreadLocalContextWebrtc.logMessage("CallSignaling: Connecting...") + self.connection.start(queue: self.queue.queue) self.receivePacketHeader() } @@ -1399,48 +1421,91 @@ private final class CallSignalingConnectionImpl: CallSignalingConnection { } private final class CallSignalingConnectionManager { + private final class ConnectionContext { + let connection: CallSignalingConnection + let host: String + let port: UInt16 + + init(connection: CallSignalingConnection, host: String, port: UInt16) { + self.connection = connection + self.host = host + self.port = port + } + } + private let queue: Queue + private let peerTag: Data + private let dataReceived: (Data) -> Void + + private var isRunning: Bool = false private var nextConnectionId: Int = 0 - private var connections: [Int: CallSignalingConnection] = [:] + private var connections: [Int: ConnectionContext] = [:] init(queue: Queue, peerTag: Data, servers: [OngoingCallConnectionDescriptionWebrtc], dataReceived: @escaping (Data) -> Void) { self.queue = queue + self.peerTag = peerTag + self.dataReceived = dataReceived for server in servers { if server.hasTcp { - let id = self.nextConnectionId - self.nextConnectionId += 1 - if #available(iOS 12.0, *) { - let connection = CallSignalingConnectionImpl(queue: queue, host: server.ip, port: UInt16(server.port), peerTag: peerTag, dataReceived: { data in - dataReceived(data) - }, isClosed: { [weak self] in - guard let strongSelf = self else { - return - } - let _ = strongSelf - }) - connections[id] = connection - } + self.spawnConnection(host: server.ip, port: UInt16(server.port)) } } } func start() { + if self.isRunning { + return + } + self.isRunning = true + for (_, connection) in self.connections { - connection.start() + connection.connection.start() } } func stop() { + if !self.isRunning { + return + } + self.isRunning = false + for (_, connection) in self.connections { - connection.stop() + connection.connection.stop() } } func send(payloadData: Data) { for (_, connection) in self.connections { - connection.send(payloadData: payloadData) + connection.connection.send(payloadData: payloadData) + } + } + + private func spawnConnection(host: String, port: UInt16) { + let id = self.nextConnectionId + self.nextConnectionId += 1 + if #available(iOS 12.0, *) { + let dataReceived = self.dataReceived + let connection = CallSignalingConnectionImpl(queue: queue, host: host, port: port, peerTag: self.peerTag, dataReceived: { data in + dataReceived(data) + }, isClosed: { [weak self] in + guard let self else { + return + } + self.handleConnectionFailed(id: id) + }) + self.connections[id] = ConnectionContext(connection: connection, host: host, port: port) + if self.isRunning { + connection.start() + } + } + } + + private func handleConnectionFailed(id: Int) { + if let connection = self.connections.removeValue(forKey: id) { + connection.connection.stop() + self.spawnConnection(host: connection.host, port: connection.port) } } } diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index d75206844c..ea6855191d 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -206,7 +206,24 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { @property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t); @property (nonatomic, copy) void (^ _Nullable audioLevelUpdated)(float); -- (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing connections:(NSArray * _Nonnull)connections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P allowTCP:(BOOL)allowTCP enableStunMarking:(BOOL)enableStunMarking logPath:(NSString * _Nonnull)logPath statsLogPath:(NSString * _Nonnull)statsLogPath sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec audioInputDeviceId: (NSString * _Nonnull)audioInputDeviceId; +- (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id _Nonnull)queue + proxy:(VoipProxyServerWebrtc * _Nullable)proxy + networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving + derivedState:(NSData * _Nonnull)derivedState + key:(NSData * _Nonnull)key + isOutgoing:(bool)isOutgoing + connections:(NSArray * _Nonnull)connections maxLayer:(int32_t)maxLayer + allowP2P:(BOOL)allowP2P + allowTCP:(BOOL)allowTCP + enableStunMarking:(BOOL)enableStunMarking + logPath:(NSString * _Nonnull)logPath + statsLogPath:(NSString * _Nonnull)statsLogPath + sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer + preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec + audioInputDeviceId:(NSString * _Nonnull)audioInputDeviceId + useManualAudioSessionControl:(bool)useManualAudioSessionControl; + +- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive; - (void)beginTermination; - (void)stop:(void (^_Nullable)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile))completion; @@ -360,6 +377,8 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { - (void)stop; +- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive; + - (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled isUnifiedBroadcast:(bool)isUnifiedBroadcast; - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 32c1ddb7d2..24a0167e99 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -28,6 +28,9 @@ #include "platform/darwin/iOS/tgcalls_audio_device_module_ios.h" +#include "platform/darwin/iOS/RTCAudioSession.h" +#include "platform/darwin/iOS/RTCAudioSessionConfiguration.h" + #endif #import "group/GroupInstanceImpl.h" @@ -705,6 +708,8 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls: id _queue; int32_t _contextId; + bool _useManualAudioSessionControl; + OngoingCallNetworkTypeWebrtc _networkType; NSTimeInterval _callReceiveTimeout; NSTimeInterval _callRingTimeout; @@ -843,7 +848,22 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } } -- (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing connections:(NSArray * _Nonnull)connections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P allowTCP:(BOOL)allowTCP enableStunMarking:(BOOL)enableStunMarking logPath:(NSString * _Nonnull)logPath statsLogPath:(NSString * _Nonnull)statsLogPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec audioInputDeviceId: (NSString * _Nonnull)audioInputDeviceId { +- (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id _Nonnull)queue + proxy:(VoipProxyServerWebrtc * _Nullable)proxy + networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving + derivedState:(NSData * _Nonnull)derivedState + key:(NSData * _Nonnull)key + isOutgoing:(bool)isOutgoing + connections:(NSArray * _Nonnull)connections maxLayer:(int32_t)maxLayer + allowP2P:(BOOL)allowP2P + allowTCP:(BOOL)allowTCP + enableStunMarking:(BOOL)enableStunMarking + logPath:(NSString * _Nonnull)logPath + statsLogPath:(NSString * _Nonnull)statsLogPath + sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer + preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec + audioInputDeviceId:(NSString * _Nonnull)audioInputDeviceId + useManualAudioSessionControl:(bool)useManualAudioSessionControl { self = [super init]; if (self != nil) { _version = version; @@ -852,6 +872,25 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; assert([[OngoingCallThreadLocalContextWebrtc versionsWithIncludeReference:true] containsObject:version]); + _useManualAudioSessionControl = useManualAudioSessionControl; + [RTCAudioSession sharedInstance].useManualAudio = true; + +#ifdef WEBRTC_IOS + RTCAudioSessionConfiguration *sharedConfiguration = [RTCAudioSessionConfiguration webRTCConfiguration]; + if (useManualAudioSessionControl) { + sharedConfiguration.mode = AVAudioSessionModeDefault; + } else { + sharedConfiguration.mode = AVAudioSessionModeVoiceChat; + } + sharedConfiguration.categoryOptions |= AVAudioSessionCategoryOptionMixWithOthers; + sharedConfiguration.outputNumberOfChannels = 1; + [RTCAudioSessionConfiguration setWebRTCConfiguration:sharedConfiguration]; + + /*[RTCAudioSession sharedInstance].useManualAudio = true; + [[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]]; + [RTCAudioSession sharedInstance].isAudioEnabled = true;*/ +#endif + _callReceiveTimeout = 20.0; _callRingTimeout = 90.0; _callConnectTimeout = 30.0; @@ -1094,6 +1133,17 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; - (void)beginTermination { } +- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive { + if (_useManualAudioSessionControl) { + if (isAudioSessionActive) { + [[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]]; + } else { + [[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]]; + } + [RTCAudioSession sharedInstance].isAudioEnabled = isAudioSessionActive; + } +} + + (void)stopWithTerminationResult:(OngoingCallThreadLocalContextWebrtcTerminationResult *)terminationResult completion:(void (^)(NSString *, int64_t, int64_t, int64_t, int64_t))completion { if (completion) { if (terminationResult) { @@ -1429,6 +1479,22 @@ private: } } +#ifdef WEBRTC_IOS + RTCAudioSessionConfiguration *sharedConfiguration = [RTCAudioSessionConfiguration webRTCConfiguration]; + sharedConfiguration.mode = AVAudioSessionModeVoiceChat; + sharedConfiguration.categoryOptions |= AVAudioSessionCategoryOptionMixWithOthers; + if (disableAudioInput) { + sharedConfiguration.outputNumberOfChannels = 2; + } else { + sharedConfiguration.outputNumberOfChannels = 1; + } + [RTCAudioSessionConfiguration setWebRTCConfiguration:sharedConfiguration]; + + /*[RTCAudioSession sharedInstance].useManualAudio = true; + [[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]]; + [RTCAudioSession sharedInstance].isAudioEnabled = true;*/ +#endif + std::vector videoCodecPreferences; int minOutgoingVideoBitrateKbit = 500; @@ -1612,6 +1678,15 @@ private: } } +- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive { + if (isAudioSessionActive) { + [[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]]; + } else { + [[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]]; + } + [RTCAudioSession sharedInstance].isAudioEnabled = isAudioSessionActive; +} + - (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled isUnifiedBroadcast:(bool)isUnifiedBroadcast { if (_instance) { tgcalls::GroupConnectionMode mappedConnectionMode; diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 53bb1711ae..0aa4b1277f 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 53bb1711ae0b3810d34edb1c81982b18d70c5506 +Subproject commit 0aa4b1277fd018e56bf194d72b5405e397c6918b