diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0af2c94adf..e67ef92237 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7242,3 +7242,11 @@ Sorry for the inconvenience."; "Conversation.CopyProtectionForwardingDisabledBot" = "Forwards from this bot are restricted"; "Conversation.CopyProtectionSavingDisabledBot" = "Saving from this bot is restricted"; + +"Channel.ChannelSubscribersHeader" = "CHANNEL SUBSCRIBERS"; + +"Channel.Members.Contacts" = "CONTACTS IN THIS CHANNEL"; +"Channel.Members.Other" = "OTHERS SUBSCRIBERS"; + +"Group.Members.Contacts" = "CONTACTS IN THIS GROUP"; +"Group.Members.Other" = "OTHERS MEMBERS"; diff --git a/build-system/bazel-rules/apple_support b/build-system/bazel-rules/apple_support index a448644dce..c1f83903e8 160000 --- a/build-system/bazel-rules/apple_support +++ b/build-system/bazel-rules/apple_support @@ -1 +1 @@ -Subproject commit a448644dce247e59268864fd66b6578195412b59 +Subproject commit c1f83903e864d753477e51d66d3ada6c2c6d096f diff --git a/build-system/bazel-rules/rules_apple b/build-system/bazel-rules/rules_apple index 4ebd7cc45e..fcbfcfad2d 160000 --- a/build-system/bazel-rules/rules_apple +++ b/build-system/bazel-rules/rules_apple @@ -1 +1 @@ -Subproject commit 4ebd7cc45edfe6c525c1553a0f5294895653c9df +Subproject commit fcbfcfad2d633b6c8be85954975db88bee3fa26c diff --git a/build-system/bazel-rules/rules_swift b/build-system/bazel-rules/rules_swift index e850484071..03c89782e9 160000 --- a/build-system/bazel-rules/rules_swift +++ b/build-system/bazel-rules/rules_swift @@ -1 +1 @@ -Subproject commit e850484071d2b8dc41685a62d3567c312173f3c3 +Subproject commit 03c89782e9a15d467c7e036ee36f9adb6bdda910 diff --git a/build-system/tulsi b/build-system/tulsi index 4eb4edc06e..ec7dd9ddf4 160000 --- a/build-system/tulsi +++ b/build-system/tulsi @@ -1 +1 @@ -Subproject commit 4eb4edc06eef85d0a4593f36e8fe34e33f718018 +Subproject commit ec7dd9ddf4b73dedb02df827b7ab3b2cbb1f2ac0 diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 7d9e4b84e0..a7c8628a6b 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -115,7 +115,9 @@ public final class CallListController: TelegramBaseController { self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle self.tabBarItem.image = icon self.tabBarItem.selectedImage = icon - self.tabBarItem.animationName = "TabCalls" + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabCalls" + } } self.segmentedTitleView.indexUpdated = { [weak self] index in @@ -166,6 +168,11 @@ public final class CallListController: TelegramBaseController { self.segmentedTitleView.index = index self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabCalls" + } else { + self.tabBarItem.animationName = nil + } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) switch self.mode { case .tab: diff --git a/submodules/Camera/Sources/CameraPreviewNode.swift b/submodules/Camera/Sources/CameraPreviewNode.swift index 9e88730a71..019e73a47e 100644 --- a/submodules/Camera/Sources/CameraPreviewNode.swift +++ b/submodules/Camera/Sources/CameraPreviewNode.swift @@ -2,6 +2,7 @@ import Foundation import AsyncDisplayKit import Display import AVFoundation +import SwiftSignalKit private final class CameraPreviewNodeLayerNullAction: NSObject, CAAction { @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { @@ -16,16 +17,25 @@ private final class CameraPreviewNodeLayer: AVSampleBufferDisplayLayer { public final class CameraPreviewNode: ASDisplayNode { private var displayLayer: AVSampleBufferDisplayLayer + + private let fadeNode: ASDisplayNode + private var fadedIn = false public override init() { self.displayLayer = AVSampleBufferDisplayLayer() self.displayLayer.videoGravity = .resizeAspectFill + self.fadeNode = ASDisplayNode() + self.fadeNode.backgroundColor = .black + self.fadeNode.isUserInteractionEnabled = false + super.init() self.clipsToBounds = true self.layer.addSublayer(self.displayLayer) + + self.addSubnode(self.fadeNode) } func prepare() { @@ -36,6 +46,14 @@ public final class CameraPreviewNode: ASDisplayNode { func enqueue(_ sampleBuffer: CMSampleBuffer) { self.displayLayer.enqueue(sampleBuffer) + + if !self.fadedIn { + self.fadedIn = true + Queue.mainQueue().after(0.2) { + self.fadeNode.alpha = 0.0 + self.fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } } override public func layout() { @@ -46,5 +64,6 @@ public final class CameraPreviewNode: ASDisplayNode { self.displayLayer.setAffineTransform(transform) self.displayLayer.frame = self.bounds + self.fadeNode.frame = self.bounds } } diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 35c867213e..57f60eaa4d 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -26,6 +26,7 @@ public enum ChatListSearchItemHeaderType { case activeVoiceChats case recentCalls case orImportIntoAnExistingGroup + case subscribers fileprivate func title(strings: PresentationStrings) -> String { switch self { @@ -71,6 +72,8 @@ public enum ChatListSearchItemHeaderType { return strings.CallList_RecentCallsHeader case .orImportIntoAnExistingGroup: return strings.ChatList_HeaderImportIntoAnExistingGroup + case .subscribers: + return strings.Channel_ChannelSubscribersHeader } } @@ -118,6 +121,8 @@ public enum ChatListSearchItemHeaderType { return .recentCalls case .orImportIntoAnExistingGroup: return .orImportIntoAnExistingGroup + case .subscribers: + return .subscribers } } } @@ -148,6 +153,7 @@ private enum ChatListSearchItemHeaderId: Int32 { case activeVoiceChats case recentCalls case orImportIntoAnExistingGroup + case subscribers } public final class ChatListSearchItemHeader: ListViewItemHeader { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 814bf5a941..6f76cc6c36 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -206,7 +206,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.tabBarItem.image = icon self.tabBarItem.selectedImage = icon - self.tabBarItem.animationName = "TabChats" + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabChats" + self.tabBarItem.animationOffset = CGPoint(x: 0.0, y: UIScreenPixel) + } let leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) leftBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Edit @@ -519,6 +522,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back self.navigationItem.backBarButtonItem = backBarButtonItem + + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabChats" + } else { + self.tabBarItem.animationName = nil + } } else { let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 6aa57f983a..0b14205861 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -192,7 +192,9 @@ public class ContactsController: ViewController { self.tabBarItem.image = icon self.tabBarItem.selectedImage = icon - self.tabBarItem.animationName = "TabContacts" + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabContacts" + } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -281,6 +283,11 @@ public class ContactsController: ViewController { self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) self.title = self.presentationData.strings.Contacts_Title self.tabBarItem.title = self.presentationData.strings.Contacts_Title + if !self.presentationData.reduceMotion { + self.tabBarItem.animationName = "TabContacts" + } else { + self.tabBarItem.animationName = nil + } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) if self.navigationItem.rightBarButtonItem != nil { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed)) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 020241696c..cbf0ec3e93 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1086,44 +1086,46 @@ public class TextNode: ASDisplayNode { } var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location } - } else if startIndex == nil { - startIndex = range.location + currentIndex = range.location + range.length } - currentIndex = range.location + range.length + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) + } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { + headIndent = paragraphStyle.headIndent + } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent - } } diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index 61d7d0137f..6b22a0c21b 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -143,7 +143,7 @@ final class GameControllerNode: ViewControllerTracingNode { if eventName == "share_game" || eventName == "share_score" { if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { if eventName == "share_score" { - self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, text, account in + self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, text, account, _ in if let strongSelf = self { let signals = peerIds.map { TelegramEngine(account: account).messages.forwardGameWithScore(messageId: strongSelf.message.id, to: $0, as: nil) } return .single(.preparing) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift index dadfc898be..2a37ff824d 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Postbox +import TelegramCore enum StickerVerificationStatus { case loading @@ -9,10 +10,39 @@ enum StickerVerificationStatus { } public class ImportStickerPack { + public enum StickerPackType { + case image + case animation + case video + + var importType: CreateStickerSetType { + switch self { + case .image: + return .image + case .animation: + return .animation + case .video: + return .video + } + } + } + public class Sticker: Equatable { public enum Content { case image(Data) case animation(Data) + case video(Data, String) + } + + var mimeType: String { + switch self.content { + case .image: + return "image/png" + case .animation: + return "application/x-tgsticker" + case let .video(_, mimeType): + return mimeType + } } public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool { @@ -32,9 +62,7 @@ public class ImportStickerPack { var data: Data { switch self.content { - case let .image(data): - return data - case let .animation(data): + case let .image(data), let .animation(data), let .video(data, _): return data } } @@ -49,7 +77,7 @@ public class ImportStickerPack { } public let software: String - public let isAnimated: Bool + public let type: StickerPackType public let thumbnail: Sticker? public let stickers: [Sticker] @@ -59,20 +87,33 @@ public class ImportStickerPack { } self.software = json["software"] as? String ?? "" let isAnimated = json["isAnimated"] as? Bool ?? false - self.isAnimated = isAnimated + let isVideo = json["isVideo"] as? Bool ?? false + let type: StickerPackType + if isAnimated { + type = .animation + } else if isVideo { + type = .video + } else { + type = .image + } + self.type = type func parseSticker(_ sticker: [String: Any]) -> Sticker? { if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) { var content: Sticker.Content? switch mimeType.lowercased() { case "image/png": - if !isAnimated { + if case .image = type { content = .image(data) } case "application/x-tgsticker": - if isAnimated { + if case .animation = type { content = .animation(data) } + case "video/webm", "image/webp", "image/gif": + if case .video = type { + content = .video(data, mimeType) + } default: break } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift index 4fb5ca1240..1f49b051f9 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -79,7 +79,8 @@ public final class ImportStickerPackController: ViewController, StandalonePresen Queue.mainQueue().after(0.1) { self.controllerNode.updateStickerPack(self.stickerPack, verifiedStickers: Set(), declinedStickers: Set(), uploadedStickerResources: [:]) - if self.stickerPack.isAnimated { + if case .image = self.stickerPack.type { + } else { let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { @@ -89,13 +90,13 @@ public final class ImportStickerPackController: ViewController, StandalonePresen var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = [] for sticker in strongSelf.stickerPack.stickers { if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] { - signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), isAnimated: true) + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType) |> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in switch result { case .progress: return (sticker.uuid, .loading, nil) case let .complete(resource, mimeType): - if mimeType == "application/x-tgsticker" { + if ["application/x-tgsticker", "video/webm"].contains(mimeType) { return (sticker.uuid, .verified, resource) } else { return (sticker.uuid, .declined, nil) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index e84e2685b1..15b6145ccb 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -607,9 +607,9 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll if let localResource = item.stickerItem.resource { self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id) } - stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions)) + stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType)) } else if let resource = item.stickerItem.resource { - stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions)) + stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType)) } } var thumbnailSticker: ImportSticker? @@ -620,7 +620,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll } let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) - thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions) + thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType) } let firstStickerItem = thumbnailSticker ?? stickers.first @@ -631,13 +631,17 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) } - self.disposable.set((self.context.engine.stickers.createStickerSet(title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnailSticker, isAnimated: stickerPack.isAnimated, software: stickerPack.software) + self.disposable.set((self.context.engine.stickers.createStickerSet(title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnailSticker, type: stickerPack.type.importType, software: stickerPack.software) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { if case let .complete(info, items) = status { if let (_, _, count) = strongSelf.progress { strongSelf.progress = (1.0, count, count) - strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: !stickerPack.isAnimated, synchronous: true, completion: {}) + var animated = false + if case .image = stickerPack.type { + animated = true + } + strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: animated, synchronous: true, completion: {}) if let (layout, navigationBarHeight) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } @@ -675,7 +679,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll Queue.mainQueue().after(1.0) { var firstItem: StickerPackItem? if let firstStickerItem = firstStickerItem, let resource = firstStickerItem.resource as? TelegramMediaResource { - firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: stickerPack.isAnimated ? "application/x-tgsticker": "image/png", size: nil, attributes: [.FileName(fileName: stickerPack.isAnimated ? "sticker.tgs" : "sticker.png"), .ImageSize(size: firstStickerItem.dimensions)]), indexKeys: []) + firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: firstStickerItem.mimeType, size: nil, attributes: [.FileName(fileName: ""), .ImageSize(size: firstStickerItem.dimensions)]), indexKeys: []) } strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in if case .info = action { @@ -781,11 +785,16 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll item.resource = resource self.stickerResources[item.uuid] = resource } - updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item, isVerified: !item.isAnimated || verifiedStickers.contains(item.uuid))) + var isInitiallyVerified = false + if case .image = item.content { + isInitiallyVerified = true + } + updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item, isVerified: isInitiallyVerified || verifiedStickers.contains(item.uuid))) } self.pendingItems = updatedItems - if stickerPack.isAnimated { + if case .image = stickerPack.type { + } else { self.stickerPackReady = stickerPack.stickers.count == (verifiedStickers.count + declinedStickers.count) && updatedItems.count > 0 } diff --git a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift index ae862e592c..7182a53a63 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift @@ -11,6 +11,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import TelegramPresentationData import ShimmerEffect +import SoftwareVideo final class StickerPackPreviewInteraction { var previewedItem: ImportStickerPack.Sticker? @@ -60,6 +61,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private var isVerified: Bool? private let imageNode: ASImageNode private var animationNode: AnimatedStickerNode? + private var videoNode: VideoStickerNode? private var placeholderNode: ShimmerEffectNode? private var theme: PresentationTheme? @@ -145,6 +147,39 @@ final class StickerPackPreviewGridItemNode: GridItemNode { let placeholderNode = ShimmerEffectNode() self.placeholderNode = placeholderNode + self.addSubnode(placeholderNode) + if let (absoluteRect, containerSize) = self.absoluteLocation { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + case .video: + self.imageNode.isHidden = true + + if isVerified { + let videoNode = VideoStickerNode() + self.videoNode = videoNode + + if let resource = stickerItem.resource as? TelegramMediaResource { + let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])]) + + videoNode.update(account: account, fileReference: .standalone(media: dummyFile)) + } + + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + self.insertSubnode(videoNode, belowSubnode: placeholderNode) + } else { + self.addSubnode(videoNode) + } + + videoNode.update(isPlaying: self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true) + } else { + let placeholderNode = ShimmerEffectNode() + self.placeholderNode = placeholderNode + self.addSubnode(placeholderNode) if let (absoluteRect, containerSize) = self.absoluteLocation { placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) @@ -175,6 +210,11 @@ final class StickerPackPreviewGridItemNode: GridItemNode { animationNode.updateLayout(size: imageSize) } + if let videoNode = self.videoNode { + videoNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + videoNode.updateLayout(size: imageSize) + } + if let placeholderNode = self.placeholderNode, let theme = self.theme { placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 11.0)], horizontal: true, size: imageSize) placeholderNode.frame = self.imageNode.frame diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index cac2126712..fc67597ed2 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -9,6 +9,7 @@ import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode import ContextUI +import SoftwareVideo public final class StickerPreviewPeekContent: PeekControllerContent { let account: Account @@ -57,6 +58,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController private var textNode: ASTextNode private var imageNode: ASImageNode private var animationNode: AnimatedStickerNode? + private var videoNode: VideoStickerNode? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -79,6 +81,16 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) } self.animationNode?.visibility = true + case .video: + let videoNode = VideoStickerNode() + self.videoNode = videoNode + + if let resource = item.resource as? TelegramMediaResource { + let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: resource.size ?? 1, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])]) + + videoNode.update(account: account, fileReference: .standalone(media: dummyFile)) + } + videoNode.update(isPlaying: true) } if case let .image(data) = item.content, let image = UIImage(data: data) { self.imageNode.image = image @@ -89,7 +101,9 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.isUserInteractionEnabled = false - if let animationNode = self.animationNode { + if let videoNode = self.videoNode { + self.addSubnode(videoNode) + } else if let animationNode = self.animationNode { self.addSubnode(animationNode) } else { self.addSubnode(self.imageNode) @@ -108,7 +122,10 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.imageNode.frame = imageFrame - if let animationNode = self.animationNode { + if let videoNode = self.videoNode { + videoNode.frame = imageFrame + videoNode.updateLayout(size: imageFrame.size) + } else if let animationNode = self.animationNode { animationNode.frame = imageFrame animationNode.updateLayout(size: imageFrame.size) } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index feeb97d394..0ff3170822 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -59,22 +59,29 @@ public class InvisibleInkDustNode: ASDisplayNode { public var isRevealed = false + private var exploding = false + public init(textNode: TextNode?) { self.textNode = textNode self.emitterNode = ASDisplayNode() + self.emitterNode.isUserInteractionEnabled = false self.emitterNode.clipsToBounds = true self.textMaskNode = ASDisplayNode() + self.textMaskNode.isUserInteractionEnabled = false self.textSpotNode = ASImageNode() self.textSpotNode.contentMode = .scaleToFill + self.textSpotNode.isUserInteractionEnabled = false self.emitterMaskNode = ASDisplayNode() self.emitterSpotNode = ASImageNode() self.emitterSpotNode.contentMode = .scaleToFill + self.emitterSpotNode.isUserInteractionEnabled = false self.emitterMaskFillNode = ASDisplayNode() self.emitterMaskFillNode.backgroundColor = .white + self.emitterMaskFillNode.isUserInteractionEnabled = false super.init() @@ -135,7 +142,7 @@ public class InvisibleInkDustNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))) } - public func update(revealed: Bool) { + public func update(revealed: Bool, animated: Bool = true) { guard self.isRevealed != revealed, let textNode = self.textNode else { return } @@ -143,13 +150,18 @@ public class InvisibleInkDustNode: ASDisplayNode { self.isRevealed = revealed if revealed { - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 0.0) transition.updateAlpha(node: textNode, alpha: 1.0) } else { - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 1.0) transition.updateAlpha(node: textNode, alpha: 0.0) + + if self.exploding { + self.exploding = false + self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + } } } @@ -159,6 +171,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } self.isRevealed = true + self.exploding = true let position = gestureRecognizer.location(in: self.view) self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") @@ -214,6 +227,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { + self.exploding = false self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") self.textSpotNode.layer.removeAllAnimations() diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 16ece5bc68..8c66f0c2ca 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -1,5 +1,9 @@ import Foundation +#if !os(macOS) import UIKit +#else +import AppKit +#endif import CoreMedia import SwiftSignalKit import FFMpegBinding diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 8bd13e88f8..4fed8b6d85 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -1,5 +1,9 @@ import Foundation +#if !os(macOS) import UIKit +#else +import AppKit +#endif import SwiftSignalKit import Postbox import TelegramCore diff --git a/submodules/OpenSSLEncryptionProvider/Package.swift b/submodules/OpenSSLEncryptionProvider/Package.swift new file mode 100644 index 0000000000..c70a67dc33 --- /dev/null +++ b/submodules/OpenSSLEncryptionProvider/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + + +let package = Package( + name: "OpenSSLEncryption", + platforms: [ + .macOS(.v10_11) + ], + products: [ + .library( + name: "OpenSSLEncryption", + targets: ["OpenSSLEncryption"]), + ], + targets: [ + .target( + name: "OpenSSLEncryption", + dependencies: [], + path: ".", + exclude: ["BUILD"], + publicHeadersPath: "PublicHeaders", + cSettings: [ + .headerSearchPath("PublicHeaders"), + .unsafeFlags([ + "-I../../../../core-xprojects/openssl/build/openssl/include", + "-I../EncryptionProvider/PublicHeaders" + ]) + ]), + ] +) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 4ad1acfcd9..60698d1b76 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -36,6 +36,7 @@ private final class ChannelMembersControllerArguments { private enum ChannelMembersSection: Int32 { case addMembers + case contacts case peers } @@ -48,14 +49,20 @@ private enum ChannelMembersEntry: ItemListNodeEntry { case addMember(PresentationTheme, String) case addMemberInfo(PresentationTheme, String) case inviteLink(PresentationTheme, String) - case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + case contactsTitle(PresentationTheme, String) + case peersTitle(PresentationTheme, String) + case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, RenderedChannelParticipant, ItemListPeerItemEditing, Bool, Bool) var section: ItemListSectionId { switch self { case .addMember, .addMemberInfo, .inviteLink: return ChannelMembersSection.addMembers.rawValue - case .peerItem: + case .contactsTitle: + return ChannelMembersSection.contacts.rawValue + case .peersTitle: return ChannelMembersSection.peers.rawValue + case let .peerItem(_, _, _, _, _, _, _, _, isContact): + return isContact ? ChannelMembersSection.contacts.rawValue : ChannelMembersSection.peers.rawValue } } @@ -65,9 +72,13 @@ private enum ChannelMembersEntry: ItemListNodeEntry { return .index(0) case .addMemberInfo: return .index(1) - case .inviteLink: - return .index(2) - case let .peerItem(_, _, _, _, _, participant, _, _): + case .inviteLink: + return .index(2) + case .contactsTitle: + return .index(3) + case .peersTitle: + return .index(4) + case let .peerItem(_, _, _, _, _, participant, _, _, _): return .peer(participant.peer.id) } } @@ -86,14 +97,26 @@ private enum ChannelMembersEntry: ItemListNodeEntry { } else { return false } - case let .inviteLink(lhsTheme, lhsText): - if case let .inviteLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + case let .inviteLink(lhsTheme, lhsText): + if case let .inviteLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .contactsTitle(lhsTheme, lhsText): + if case let .contactsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peersTitle(lhsTheme, lhsText): + if case let .peersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsParticipant, lhsEditing, lhsEnabled, lhsIsContact): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsParticipant, rhsEditing, rhsEnabled, rhsIsContact) = rhs { if lhsIndex != rhsIndex { return false } @@ -118,6 +141,9 @@ private enum ChannelMembersEntry: ItemListNodeEntry { if lhsEnabled != rhsEnabled { return false } + if lhsIsContact != rhsIsContact { + return false + } return true } else { return false @@ -143,11 +169,30 @@ private enum ChannelMembersEntry: ItemListNodeEntry { default: return true } - - case let .peerItem(index, _, _, _, _, _, _, _): + case .contactsTitle: switch rhs { - case let .peerItem(rhsIndex, _, _, _, _, _, _, _): - return index < rhsIndex + case .addMember, .addMemberInfo, .inviteLink: + return false + default: + return true + } + case .peersTitle: + switch rhs { + case .addMember, .addMemberInfo, .inviteLink, .contactsTitle: + return false + case let .peerItem(_, _, _, _, _, _, _, _, isContact): + return !isContact + default: + return true + } + case let .peerItem(lhsIndex, _, _, _, _, _, _, _, lhsIsContact): + switch rhs { + case .contactsTitle: + return false + case .peersTitle: + return lhsIsContact + case let .peerItem(rhsIndex, _, _, _, _, _, _, _, _): + return lhsIndex < rhsIndex case .addMember, .addMemberInfo, .inviteLink: return false } @@ -167,7 +212,9 @@ private enum ChannelMembersEntry: ItemListNodeEntry { }) case let .addMemberInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled): + case let .contactsTitle(_, text), let .peersTitle(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled, _): let text: ItemListPeerItemText if let user = participant.peer as? TelegramUser, let _ = user.botInfo { text = .text(strings.Bot_GenericBotStatus, .secondary) @@ -238,7 +285,7 @@ private struct ChannelMembersControllerState: Equatable { } } -private func channelMembersControllerEntries(context: AccountContext, presentationData: PresentationData, view: PeerView, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?, isGroup: Bool) -> [ChannelMembersEntry] { +private func channelMembersControllerEntries(context: AccountContext, presentationData: PresentationData, view: PeerView, state: ChannelMembersControllerState, contacts: [RenderedChannelParticipant]?, participants: [RenderedChannelParticipant]?, isGroup: Bool) -> [ChannelMembersEntry] { if participants == nil || participants?.count == nil { return [] } @@ -251,6 +298,11 @@ private func channelMembersControllerEntries(context: AccountContext, presentati canAddMember = peer.hasPermission(.inviteMembers) } + var canEditMembers = false + if let peer = view.peers[view.peerId] as? TelegramChannel { + canEditMembers = peer.hasPermission(.banMembers) + } + if canAddMember { entries.append(.addMember(presentationData.theme, isGroup ? presentationData.strings.Group_Members_AddMembers : presentationData.strings.Channel_Members_AddMembers)) if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil { @@ -267,14 +319,44 @@ private func channelMembersControllerEntries(context: AccountContext, presentati var index: Int32 = 0 - let sortedParticipants = participants - for participant in sortedParticipants { - var editable = true - var canEditMembers = false - if let peer = view.peers[view.peerId] as? TelegramChannel { - canEditMembers = peer.hasPermission(.banMembers) + var existingPeerIds = Set() + + var addedContactsHeader = false + if let contacts = contacts, !contacts.isEmpty { + addedContactsHeader = true + + entries.append(.contactsTitle(presentationData.theme, isGroup ? presentationData.strings.Group_Members_Contacts : presentationData.strings.Channel_Members_Contacts)) + + for participant in contacts { + var editable = true + if participant.peer.id == context.account.peerId { + editable = false + } else { + switch participant.participant { + case .creator: + editable = false + case .member: + editable = canEditMembers + } + } + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, true)) + existingPeerIds.insert(participant.peer.id) + index += 1 + } + } + + var addedOtherHeader = false + for participant in participants { + if existingPeerIds.contains(participant.peer.id) { + continue } + if addedContactsHeader && !addedOtherHeader { + addedOtherHeader = true + entries.append(.peersTitle(presentationData.theme, isGroup ? presentationData.strings.Group_Members_Other : presentationData.strings.Channel_Members_Other)) + } + + var editable = true if participant.peer.id == context.account.peerId { editable = false } else { @@ -285,7 +367,7 @@ private func channelMembersControllerEntries(context: AccountContext, presentati editable = canEditMembers } } - entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, false)) index += 1 } } @@ -315,6 +397,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio actionsDisposable.add(removePeerDisposable) let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) + let contactsPromise = Promise<[RenderedChannelParticipant]?>(nil) let arguments = ChannelMembersControllerArguments(context: context, addMember: { actionsDisposable.add((peersPromise.get() @@ -437,17 +520,22 @@ public func channelMembersController(context: AccountContext, updatedPresentatio let peerView = context.account.viewTracker.peerView(peerId) + let (contactsDisposable, _) = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in + contactsPromise.set(.single(state.list)) + }) let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in peersPromise.set(.single(state.list)) }) actionsDisposable.add(disposable) + actionsDisposable.add(contactsDisposable) - var previousPeers: [RenderedChannelParticipant]? + var currentContacts: [RenderedChannelParticipant]? + var currentPeers: [RenderedChannelParticipant]? let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), peerView, peersPromise.get()) + let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), peerView, contactsPromise.get(), peersPromise.get()) |> deliverOnMainQueue - |> map { presentationData, state, view, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, view, contacts, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in var isGroup = true if let peer = peerViewMainPeer(view) as? TelegramChannel, case .broadcast = peer.info { isGroup = false @@ -455,7 +543,14 @@ public func channelMembersController(context: AccountContext, updatedPresentatio var rightNavigationButton: ItemListNavigationButton? var secondaryRightNavigationButton: ItemListNavigationButton? - if let peers = peers, !peers.isEmpty { + + var isEmpty = true + if let contacts = contacts, !contacts.isEmpty { + isEmpty = false + } else if let peers = peers, !peers.isEmpty { + isEmpty = false + } + if !isEmpty { if state.editing { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in @@ -497,15 +592,26 @@ public func channelMembersController(context: AccountContext, updatedPresentatio } var emptyStateItem: ItemListControllerEmptyStateItem? - if peers == nil || peers?.count == 0 { + if isEmpty { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let previous = previousPeers - previousPeers = peers + let previousContacts = currentContacts + currentContacts = contacts + + let previousPeers = currentPeers + currentPeers = peers + + var animateChanges = false + if let previousContacts = previousContacts, let contacts = contacts, previousContacts.count >= contacts.count { + animateChanges = true + } + if let previousPeers = previousPeers, let peers = peers, previousPeers.count >= peers.count { + animateChanges = true + } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isGroup ? presentationData.strings.Group_Members_Title : presentationData.strings.Channel_Subscribers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, participants: peers, isGroup: isGroup), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, contacts: contacts, participants: peers, isGroup: isGroup), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index a160580a91..4c27952373 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -38,14 +38,14 @@ private enum ChannelMembersSearchEntryId: Hashable { private enum ChannelMembersSearchEntry: Comparable, Identifiable { case copyInviteLink - case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool) + case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool, Bool, Bool) case contact(Int, Peer, TelegramUserPresence?) var stableId: ChannelMembersSearchEntryId { switch self { case .copyInviteLink: return .copyInviteLink - case let .peer(_, participant, _, _, _): + case let .peer(_, participant, _, _, _, _, _): return .peer(participant.peer.id) case let .contact(_, peer, _): return .contact(peer.id) @@ -60,8 +60,8 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled): - if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled) = rhs { + case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled, lhsIsChannel, lhsIsContact): + if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled, lhsIsChannel, lhsIsContact) = rhs { return true } else { return false @@ -92,10 +92,10 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } else { return true } - case let .peer(lhsIndex, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _, _, _): if case .copyInviteLink = rhs { return false - } else if case let .peer(rhsIndex, _, _, _, _) = rhs { + } else if case let .peer(rhsIndex, _, _, _, _, _, _) = rhs { return lhsIndex < rhsIndex } else if case .contact = rhs { return true @@ -127,7 +127,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.VoiceChat_CopyInviteLink, icon: icon, clearHighlightAutomatically: true, header: nil, action: { interaction.copyInviteLink() }) - case let .peer(_, participant, editing, label, enabled): + case let .peer(_, participant, editing, label, enabled, isChannel, isContact): let status: ContactsPeerItemStatus if let label = label { status = .custom(string: label, multiline: false) @@ -138,7 +138,14 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { status = .none } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(participant.peer), chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: .groupMembers, theme: presentationData.theme, strings: presentationData.strings), action: { _ in + let headerType: ChatListSearchItemHeaderType + if isContact { + headerType = .contacts + } else { + headerType = isChannel ? .subscribers : .groupMembers + } + + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(participant.peer), chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in interaction.openPeer(participant.peer, participant) }) case let .contact(_, peer, presence): @@ -239,6 +246,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { let previousEntries = Atomic<[ChannelMembersSearchEntry]?>(value: nil) let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) + let contactsDisposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)? let additionalDisposable = MetaDisposable() if peerId.namespace == Namespaces.Peer.CloudGroup { @@ -398,7 +406,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers, presences: peerView.peerPresences) } - entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) + entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false)) index += 1 } @@ -420,6 +428,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) }) disposableAndLoadMoreControl = (disposable, nil) + contactsDisposableAndLoadMoreControl = nil } else { let membersState = Promise() @@ -427,19 +436,26 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { membersState.set(.single(state)) }) + let contactsState = Promise() + contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in + contactsState.set(.single(state)) + }) + additionalDisposable.set((combineLatest(queue: .mainQueue(), membersState.get(), + contactsState.get(), context.account.postbox.peerView(id: peerId), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) ) - ).start(next: { [weak self] state, peerView, contactsView in + ).start(next: { [weak self] state, contactsState, peerView, contactsView in guard let strongSelf = self else { return } var entries: [ChannelMembersSearchEntry] = [] var canInviteByLink = false + var isChannel = false if let peer = peerViewMainPeer(peerView) { if !(peer.addressName?.isEmpty ?? true) { canInviteByLink = true @@ -447,6 +463,9 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { if peer.flags.contains(.isCreator) || (peer.adminRights?.rights.contains(.canInviteUsers) == true) { canInviteByLink = true } + if case .broadcast = peer.info { + isChannel = true + } } else if let peer = peer as? TelegramGroup { if case .creator = peer.role { canInviteByLink = true @@ -456,6 +475,8 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } } + var index = 0 + var existingPeersIds = Set() if case .inviteToCall = mode, canInviteByLink, !filters.contains(where: { filter in if case .excludeNonMembers = filter { return true @@ -464,18 +485,53 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } }) { entries.append(.copyInviteLink) + } else { + contactsLoop: for participant in contactsState.list { + if participant.peer.isDeleted { + continue contactsLoop + } + + var label: String? + var enabled = true + for filter in filters { + switch filter { + case let .exclude(ids): + if ids.contains(participant.peer.id) { + continue contactsLoop + } + case let .disable(ids): + if ids.contains(participant.peer.id) { + enabled = false + } + case .excludeNonMembers: + break + case .excludeBots: + if let user = participant.peer as? TelegramUser, user.botInfo != nil { + continue contactsLoop + } + } + } + if case .promote = mode, case .creator = participant.participant { + label = strongSelf.presentationData.strings.Channel_Management_LabelOwner + enabled = false + } + + entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, isChannel, true)) + index += 1 + + existingPeersIds.insert(participant.peer.id) + } } - var index = 0 participantsLoop: for participant in state.list { - if participant.peer.isDeleted { + if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) { continue participantsLoop } var label: String? var enabled = true switch mode { - case .ban: + case .ban, .promote: if participant.peer.id == context.account.peerId { continue participantsLoop } @@ -497,29 +553,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } } } - case .promote: - if participant.peer.id == context.account.peerId { - continue - } - for filter in filters { - switch filter { - case let .exclude(ids): - if ids.contains(participant.peer.id) { - continue participantsLoop - } - case let .disable(ids): - if ids.contains(participant.peer.id) { - enabled = false - } - case .excludeNonMembers: - break - case .excludeBots: - if let user = participant.peer as? TelegramUser, user.botInfo != nil { - continue participantsLoop - } - } - } - if case .creator = participant.participant { + if case .promote = mode, case .creator = participant.participant { label = strongSelf.presentationData.strings.Channel_Management_LabelOwner enabled = false } @@ -549,7 +583,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } } } - entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) + entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, isChannel, false)) index += 1 } @@ -575,6 +609,9 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { let combinedDisposable = DisposableSet() combinedDisposable.add(disposableAndLoadMoreControl.0) combinedDisposable.add(additionalDisposable) + if let disposable = contactsDisposableAndLoadMoreControl?.0 { + combinedDisposable.add(disposable) + } self.disposable = combinedDisposable self.listControl = disposableAndLoadMoreControl.1 diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index b4f614a1d7..dcdd97a66a 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -61,7 +61,7 @@ public enum ShareControllerSubject { case image([ImageRepresentationWithReference]) case media(AnyMediaReference) case mapMedia(TelegramMediaMap) - case fromExternal(([PeerId], String, Account) -> Signal) + case fromExternal(([PeerId], String, Account, Bool) -> Signal) } private enum ExternalShareItem { @@ -503,7 +503,7 @@ public final class ShareController: ViewController { }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues) self.controllerNode.completed = self.completed self.controllerNode.present = { [weak self] c in - self?.presentInGlobalOverlay(c, with: nil) + self?.present(c, in: .window(.root)) } self.controllerNode.dismiss = { [weak self] shared in self?.dismissed?(shared) @@ -623,7 +623,7 @@ public final class ShareController: ViewController { shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messagesToEnqueue)) } case let .fromExternal(f): - return f(peerIds, text, strongSelf.currentAccount) + return f(peerIds, text, strongSelf.currentAccount, silently) |> map { state -> ShareState in switch state { case .preparing: diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index f3bf10b027..5be75c6259 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -191,29 +191,35 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.actionButtonNode.contextAction = { [weak self] node, gesture in if let strongSelf = self, let context = strongSelf.context, let node = node as? ContextReferenceContentNode { let presentationData = strongSelf.presentationData + let fromForeignApp = strongSelf.fromForeignApp let items: Signal = strongSelf.showNames.get() |> map { showNamesValue in - return ContextController.Items(content: .list([ - .action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowSendersName, icon: { theme in - if showNamesValue { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } else { - return nil - } - }, action: { _, _ in - self?.showNames.set(true) - })), - .action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideSendersName, icon: { theme in - if !showNamesValue { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } else { - return nil - } - }, action: { _, _ in - self?.showNames.set(false) - })), - .separator, + var items: [ContextMenuItem] = [] + if !fromForeignApp { + items.append(contentsOf: [ + .action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowSendersName, icon: { theme in + if showNamesValue { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, _ in + self?.showNames.set(true) + })), + .action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideSendersName, icon: { theme in + if !showNamesValue { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, _ in + self?.showNames.set(false) + })), + .separator, + ]) + } + items.append(contentsOf: [ .action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) if let strongSelf = self { @@ -225,8 +231,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate if let strongSelf = self { strongSelf.send(showNames: showNamesValue, silently: false) } - })), - ])) + })) + ]) + return ContextController.Items(content: .list(items)) } let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(ShareContextReferenceContentSource(sourceNode: node, customPosition: CGPoint(x: 0.0, y: -116.0))), items: items, gesture: gesture) contextController.immediateItemsTransitionAnimation = true diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 2af74b474b..50639b606d 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -365,7 +365,7 @@ public func preparedShareItems(account: Account, to peerId: PeerId, dataItems: [ }) } -public func sentShareItems(account: Account, to peerIds: [PeerId], items: [PreparedShareItemContent]) -> Signal { +public func sentShareItems(account: Account, to peerIds: [PeerId], items: [PreparedShareItemContent], silently: Bool) -> Signal { var messages: [EnqueueMessage] = [] var groupingKey: Int64? var mediaTypes: (photo: Int, video: Int, music: Int, other: Int) = (0, 0, 0, 0) @@ -401,15 +401,20 @@ public func sentShareItems(account: Account, to peerIds: [PeerId], items: [Prepa groupingKey = Int64.random(in: Int64.min ... Int64.max) } + var attributes: [MessageAttribute] = [] + if silently { + attributes.append(NotificationInfoMessageAttribute(flags: .muted)) + } + var mediaMessages: [EnqueueMessage] = [] for item in items { switch item { case let .text(text): - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) + messages.append(.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) case let .media(media): switch media { case let .media(reference): - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: reference, replyToMessageId: nil, localGroupingKey: groupingKey, correlationId: nil) + let message: EnqueueMessage = .message(text: "", attributes: attributes, mediaReference: reference, replyToMessageId: nil, localGroupingKey: groupingKey, correlationId: nil) messages.append(message) mediaMessages.append(message) diff --git a/submodules/TabBarUI/Sources/TabBarNode.swift b/submodules/TabBarUI/Sources/TabBarNode.swift index 9e5069d040..9a9cc97ce5 100644 --- a/submodules/TabBarUI/Sources/TabBarNode.swift +++ b/submodules/TabBarUI/Sources/TabBarNode.swift @@ -654,10 +654,11 @@ class TabBarNode: ASDisplayNode { let scaleFactor: CGFloat = horizontal ? 0.8 : 1.0 node.animationContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0) + let animationOffset: CGPoint = self.tabBarItems[i].item.animationOffset if horizontal { node.animationNode.frame = CGRect(origin: CGPoint(x: -10.0 - UIScreenPixel, y: -4.0 - UIScreenPixel), size: CGSize(width: 51.0, height: 51.0)) } else { - node.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 51.0) / 2.0), y: -10.0 - UIScreenPixel), size: CGSize(width: 51.0, height: 51.0)) + node.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 51.0) / 2.0), y: -10.0 - UIScreenPixel).offsetBy(dx: animationOffset.x, dy: animationOffset.y), size: CGSize(width: 51.0, height: 51.0)) } if container.badgeValue != container.appliedBadgeValue { @@ -722,7 +723,9 @@ class TabBarNode: ASDisplayNode { let previousSelectedIndex = self.selectedIndex self.itemSelected(closestNode.0, longTap, [container.imageNode.imageNode, container.imageNode.textImageNode, container.badgeContainerNode]) if previousSelectedIndex != closestNode.0 { - container.imageNode.animationNode.play() + if let selectedIndex = self.selectedIndex, let _ = self.tabBarItems[selectedIndex].item.animationName { + container.imageNode.animationNode.play() + } } } } diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index e1ef1284e6..3fb09a98ef 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -610,7 +610,7 @@ struct AccountFinalState { struct AccountReplayedFinalState { let state: AccountFinalState let addedIncomingMessageIds: [MessageId] - let addedReactionEvents: [(reactionAuthor: Peer, message: Message, timestamp: Int32)] + let addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] let wasScheduledMessageIds: [MessageId] let addedSecretMessageIds: [MessageId] let deletedMessageIds: [DeletedMessageId] @@ -628,7 +628,7 @@ struct AccountReplayedFinalState { struct AccountFinalStateEvents { let addedIncomingMessageIds: [MessageId] - let addedReactionEvents: [(reactionAuthor: Peer, message: Message, timestamp: Int32)] + let addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] let wasScheduledMessageIds:[MessageId] let deletedMessageIds: [DeletedMessageId] let updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] @@ -651,7 +651,7 @@ struct AccountFinalStateEvents { return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:]) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 30d5e33023..ac9b620397 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -284,7 +284,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } switch message { - case let .message(_, _, _, replyToMessageId, _, _): + case let .message(_, attributes, _, replyToMessageId, _, _): if let replyToMessageId = replyToMessageId, replyToMessageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId) { var canBeForwarded = true if replyMessage.id.namespace != Namespaces.Message.Cloud { @@ -297,7 +297,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } if canBeForwarded { - updatedMessages.append((true, .forward(source: replyToMessageId, grouping: .none, attributes: [], correlationId: nil))) + updatedMessages.append((true, .forward(source: replyToMessageId, grouping: .none, attributes: attributes, correlationId: nil))) } } case let .forward(sourceId, _, _, _): diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 29c7106317..0dbaabe5df 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4,6 +4,43 @@ import SwiftSignalKit import TelegramApi import MtProtoKit +private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribute?, _ updatedReactions: ReactionsMessageAttribute?, message: Message, transaction: Transaction) -> (reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)? { + + if let updatedReactions = updatedReactions, !message.flags.contains(.Incoming), message.id.peerId.namespace == Namespaces.Peer.CloudUser { + let prev = previousReactions?.reactions ?? [] + + let updated = updatedReactions.reactions.filter { value in + return !prev.contains(where: { + $0.value == value.value && $0.count == value.count + }) + } + let myUpdated = updatedReactions.reactions.filter { value in + return value.isSelected + }.first + let myPrevious = prev.filter { value in + return value.isSelected + }.first + + let previousCount = prev.reduce(0, { + $0 + $1.count + }) + let updatedCount = updatedReactions.reactions.reduce(0, { + $0 + $1.count + }) + + let newReaction = updated.filter { + !$0.isSelected + }.first?.value + + if !updated.isEmpty && myUpdated == myPrevious, updatedCount >= previousCount, let value = newReaction { + if let reactionAuthor = transaction.getPeer(message.id.peerId) { + return (reactionAuthor: reactionAuthor, reaction: value, message: message, timestamp: Int32(Date().timeIntervalSince1970)) + } + } + } + return nil +} + private func peerIdsFromUpdateGroups(_ groups: [UpdateGroup]) -> Set { var peerIds = Set() @@ -2435,7 +2472,7 @@ func replayFinalState( } var wasScheduledMessageIds:[MessageId] = [] var addedIncomingMessageIds: [MessageId] = [] - var addedReactionEvents: [(reactionAuthor: Peer, message: Message, timestamp: Int32)] = [] + var addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] = [] if !wasOperationScheduledMessageIds.isEmpty { let existingIds = transaction.filterStoredMessageIds(Set(wasOperationScheduledMessageIds)) @@ -2675,6 +2712,7 @@ func replayFinalState( invalidateGroupStats.insert(Namespaces.PeerGroup.archive) } case let .EditMessage(id, message): + var generatedEvent: (reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)? transaction.updateMessage(id, update: { previousMessage in var updatedFlags = message.flags var updatedLocalTags = message.localTags @@ -2686,8 +2724,21 @@ func replayFinalState( } else { updatedFlags.remove(.Incoming) } + + let peers: [PeerId:Peer] = previousMessage.peers.reduce([:], { current, value in + var current = current + current[value.0] = value.1 + return current + }) + + if let message = locallyRenderedMessage(message: message, peers: peers) { + generatedEvent = reactionGeneratedEvent(previousMessage.reactionsAttribute, message.reactionsAttribute, message: message, transaction: transaction) + } return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags)) }) + if let generatedEvent = generatedEvent { + addedReactionEvents.append(generatedEvent) + } case let .UpdateMessagePoll(pollId, apiPoll, results): if let poll = transaction.getMedia(pollId) as? TelegramMediaPoll { var updatedPoll = poll @@ -3236,14 +3287,14 @@ func replayFinalState( return state }) } - case let .UpdateMessageReactions(messageId, reactions, eventTimestamp): - var generatedEvent: (reactionAuthor: Peer, message: Message, timestamp: Int32)? + case let .UpdateMessageReactions(messageId, reactions, _): transaction.updateMessage(messageId, update: { currentMessage in var updatedReactions = ReactionsMessageAttribute(apiReactions: reactions) let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) var attributes = currentMessage.attributes var previousReactions: ReactionsMessageAttribute? + let _ = previousReactions var added = false loop: for j in 0 ..< attributes.count { if let attribute = attributes[j] as? ReactionsMessageAttribute { @@ -3304,9 +3355,6 @@ func replayFinalState( return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) - if let generatedEvent = generatedEvent { - addedReactionEvents.append(generatedEvent) - } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index a832e6a134..46d078be08 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -105,8 +105,8 @@ public final class AccountStateManager { return self.notificationMessagesPipe.signal() } - private let reactionNotificationsPipe = ValuePipe<[(reactionAuthor: Peer, message: Message)]>() - public var reactionNotifications: Signal<[(reactionAuthor: Peer, message: Message)], NoError> { + private let reactionNotificationsPipe = ValuePipe<[(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)]>() + public var reactionNotifications: Signal<[(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)], NoError> { return self.reactionNotificationsPipe.signal() } @@ -746,16 +746,15 @@ public final class AccountStateManager { let timestamp = Int32(Date().timeIntervalSince1970) let minReactionTimestamp = timestamp - 20 - let reactionEvents = events.addedReactionEvents.compactMap { event -> (reactionAuthor: Peer, message: Message)? in + let reactionEvents = events.addedReactionEvents.compactMap { event -> (reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)? in if event.timestamp >= minReactionTimestamp { - return (event.reactionAuthor, event.message) + return (event.reactionAuthor, event.reaction, event.message, event.timestamp) } else { return nil } } - if !reactionEvents.isEmpty { - self.reactionNotificationsPipe.putNext(reactionEvents) - } + self.reactionNotificationsPipe.putNext(reactionEvents) + if !events.displayAlerts.isEmpty { self.displayAlertsPipe.putNext(events.displayAlerts) diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 416599b48b..76c19ce087 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -256,7 +256,9 @@ public extension EngineMessageReactionListContext.State { } for recentPeer in reactionsAttribute.recentPeers { if let peer = message.peers[recentPeer.peerId] { - items.append(EngineMessageReactionListContext.Item(peer: EnginePeer(peer), reaction: recentPeer.value)) + if reaction == nil || recentPeer.value == reaction { + items.append(EngineMessageReactionListContext.Item(peer: EnginePeer(peer), reaction: recentPeer.value)) + } } } } @@ -348,6 +350,8 @@ public final class EngineMessageReactionListContext { if initialState.canLoadMore { self.loadMore() + } else { + self.statePromise.set(.single(self.state)) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ProxySettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ProxySettings.swift index dc386d20d8..4413812ce7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ProxySettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ProxySettings.swift @@ -66,6 +66,7 @@ public struct ProxyServerSettings: Codable, Equatable, Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(self.host) + hasher.combine(self.port) hasher.combine(self.connection) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 099e873870..294727ca33 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -33,7 +33,7 @@ private func uploadedSticker(postbox: Postbox, network: Network, resource: Media } } -func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, isAnimated: Bool) -> Signal { +func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, mimeType: String) -> Signal { guard let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -53,7 +53,7 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) - return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: isAnimated ? "application/x-tgsticker": "image/png", attributes: attributes, stickers: nil, ttlSeconds: nil))) + return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } |> mapToSignal { media -> Signal in switch media { @@ -81,11 +81,13 @@ public struct ImportSticker { public let resource: MediaResource let emojis: [String] public let dimensions: PixelDimensions + public let mimeType: String - public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions) { + public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions, mimeType: String) { self.resource = resource self.emojis = emojis self.dimensions = dimensions + self.mimeType = mimeType } } @@ -94,7 +96,13 @@ public enum CreateStickerSetStatus { case complete(StickerPackCollectionInfo, [StickerPackItem]) } -func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, isAnimated: Bool, software: String?) -> Signal { +public enum CreateStickerSetType { + case image + case animation + case video +} + +func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal { return account.postbox.loadedPeerWithId(account.peerId) |> castError(CreateStickerSetError.self) |> mapToSignal { peer -> Signal in @@ -108,9 +116,9 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri } for sticker in stickers { if let resource = sticker.resource as? CloudDocumentMediaResource { - uploadStickers.append(.single(.complete(resource, isAnimated ? "application/x-tgsticker": "image/png"))) + uploadStickers.append(.single(.complete(resource, sticker.mimeType))) } else { - uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, isAnimated: isAnimated) + uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) |> mapError { _ -> CreateStickerSetError in return .generic }) @@ -126,7 +134,7 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri } if resources.count == stickers.count { var flags: Int32 = 0 - if isAnimated { + if case .animation = type { flags |= (1 << 1) } var inputStickers: [Api.InputStickerSetItem] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index b43bf94077..da417b8d0e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -69,12 +69,12 @@ public extension TelegramEngine { return _internal_stickerPacksAttachedToMedia(account: self.account, media: media) } - public func uploadSticker(peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, isAnimated: Bool) -> Signal { - return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, alt: alt, dimensions: dimensions, isAnimated: isAnimated) + public func uploadSticker(peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, mimeType: String) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, alt: alt, dimensions: dimensions, mimeType: mimeType) } - public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, isAnimated: Bool, software: String?) -> Signal { - return _internal_createStickerSet(account: self.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnail, isAnimated: isAnimated, software: software) + public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal { + return _internal_createStickerSet(account: self.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnail, type: type, software: software) } public func getStickerSetShortNameSuggestion(title: String) -> Signal { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index ff7393ebaf..6a13ab787c 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -322,6 +322,19 @@ public extension Message { } return nil } + var hasReactions: Bool { + for attribute in self.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + return !attribute.reactions.isEmpty + } + } + for attribute in self.attributes { + if let attribute = attribute as? PendingReactionsMessageAttribute { + return attribute.value != nil + } + } + return false + } var textEntitiesAttribute: TextEntitiesMessageAttribute? { for attribute in self.attributes { diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 6d4fa04092..7677776378 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -52,6 +52,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNode + private var spoilerTextNode: TextNode? private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode @@ -71,6 +72,15 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let queue = Queue() + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let containerResult = self.contentTextContainer.hitTest(point.offsetBy(dx: -self.contentTextContainer.frame.minX, dy: -self.contentTextContainer.frame.minY), with: event) + if containerResult?.asyncdisplaykit_node === self.dustNode, self.dustNode?.isRevealed == false { + return containerResult + } + let result = super.hitTest(point, with: event) + return result + } + init(context: AccountContext) { self.context = context @@ -270,6 +280,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentMessage = interfaceState.pinnedMessage if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { + self.dustNode?.update(revealed: false, animated: false) self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) } } @@ -352,6 +363,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let makeTitleLayout = self.titleNode.asyncLayout() let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeSpoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode) let imageNodeLayout = self.imageNode.asyncLayout() let previousMediaReference = self.previousMediaReference @@ -476,7 +488,15 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + + let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if !textLayout.spoilers.isEmpty { + spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true)) + } else { + spoilerTextLayoutAndApply = nil + } Queue.mainQueue().async { if let strongSelf = self { @@ -485,28 +505,47 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { strongSelf.previousMediaReference = updatedMediaReference - animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize())) + animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize(width: width, height: panelHeight))) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) strongSelf.textNode.frame = textFrame - if !textLayout.spoilers.isEmpty { + + if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { + let spoilerTextNode = spoilerTextApply() + if strongSelf.spoilerTextNode == nil { + spoilerTextNode.alpha = 0.0 + spoilerTextNode.isUserInteractionEnabled = false + spoilerTextNode.contentMode = .topLeft + spoilerTextNode.contentsScale = UIScreenScale + spoilerTextNode.displaysAsynchronously = false + strongSelf.contentTextContainer.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textNode) + + strongSelf.spoilerTextNode = spoilerTextNode + } + + strongSelf.spoilerTextNode?.frame = textFrame + let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: nil) - dustNode.isUserInteractionEnabled = false + dustNode = InvisibleInkDustNode(textNode: spoilerTextNode) strongSelf.dustNode = dustNode - strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) - } else if let dustNode = strongSelf.dustNode { - dustNode.removeFromSupernode() - strongSelf.dustNode = nil + } else if let spoilerTextNode = strongSelf.spoilerTextNode { + strongSelf.spoilerTextNode = nil + spoilerTextNode.removeFromSupernode() + + if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } } let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift index 4ce12005e5..14e1e627f8 100644 --- a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -2167,7 +2167,13 @@ private class MessageContentNode: ASDisplayNode, ContentNode { } } -func renderVideo(context: AccountContext, backgroundImage: UIImage, media: TelegramMediaFile, videoFrame: CGRect, completion: @escaping (URL?) -> Void) { +private enum RenderVideoResult { + case progress(Float) + case completion(URL) + case error +} + +private func renderVideo(context: AccountContext, backgroundImage: UIImage, media: TelegramMediaFile, videoFrame: CGRect, completion: @escaping (URL?) -> Void) { let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: AnyMediaReference.standalone(media: media)) |> deliverOnMainQueue).start(next: { value, isImage in guard case let .data(data) = value, data.complete else { @@ -2187,7 +2193,6 @@ func renderVideo(context: AccountContext, backgroundImage: UIImage, media: Teleg let timeRange = CMTimeRange(start: .zero, duration: duration) try compositionTrack.insertTimeRange(timeRange, of: assetTrack, at: .zero) } catch { - print(error) completion(nil) return } @@ -2228,7 +2233,6 @@ func renderVideo(context: AccountContext, backgroundImage: UIImage, media: Teleg instruction.layerInstructions = [layerInstruction] guard let export = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else { - print("Cannot create export session.") completion(nil) return } @@ -2240,13 +2244,11 @@ func renderVideo(context: AccountContext, backgroundImage: UIImage, media: Teleg export.outputURL = exportURL export.exportAsynchronously { - DispatchQueue.main.async { + Queue.mainQueue().async { switch export.status { case .completed: completion(exportURL) default: - print("Something went wrong during export.") - print(export.error ?? "unknown error") completion(nil) break } diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Sources/LegacyCamera.swift index b9e42eca07..ede0ddfc9f 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Sources/LegacyCamera.swift @@ -227,7 +227,7 @@ func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedMedia: B nativeGenerator(_1, _2, _3, nil) }) if let parentController = parentController { - parentController.present(ShareController(context: context, subject: .fromExternal({ peerIds, text, account in + parentController.present(ShareController(context: context, subject: .fromExternal({ peerIds, text, account, silently in return legacyAssetPickerEnqueueMessages(account: account, signals: signals!) |> `catch` { _ -> Signal<[LegacyAssetPickerEnqueueMessage], NoError> in return .single([]) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index c75a7aea1d..a9e16035d8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -5,6 +5,7 @@ import AccountContext import TextFormat import UIKit import AppBundle +import TelegramStringFormatting enum PeerInfoScreenLabeledValueTextColor { case primary @@ -284,16 +285,23 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + var text = item.text + let maxNumberOfLines: Int switch item.textBehavior { case .singleLine: + maxNumberOfLines = 1 + self.textNode.maximumNumberOfLines = maxNumberOfLines self.textNode.cutout = nil - self.textNode.maximumNumberOfLines = 1 self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) case let .multiLine(maxLines, enabledEntities): - self.textNode.maximumNumberOfLines = self.isExpanded ? maxLines : 3 -// self.textNode.cutout = self.isExpanded ? nil : TextNodeCutout(bottomRight: CGSize(width: expandSize.width + 4.0, height: expandSize.height)) + if !self.isExpanded { + text = trimToLineCount(text, lineCount: 3) + } + + maxNumberOfLines = self.isExpanded ? maxLines : 3 + self.textNode.maximumNumberOfLines = maxNumberOfLines if enabledEntities.isEmpty { - self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: textColorValue) } else { let fontSize: CGFloat = 17.0 @@ -304,8 +312,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { let boldItalicFont = Font.semiboldItalic(fontSize) let titleFixedFont = Font.monospace(fontSize) - let entities = generateTextEntities(item.text, enabledTypes: enabledEntities) - self.textNode.attributedText = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColorValue, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont) + let entities = generateTextEntities(text, enabledTypes: enabledEntities) + self.textNode.attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColorValue, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont) } } @@ -328,7 +336,14 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0 - additionalSideInset, height: .greatestFiniteMagnitude)) let textSize = textLayout.size - if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded { + var displayMore = false + if !self.isExpanded { + if textLayout.truncated || text.count < item.text.count { + displayMore = true + } + } + + if case .multiLine = item.textBehavior, displayMore { self.expandBackgroundNode.isHidden = false self.expandNode.isHidden = false self.expandButonNode.isHidden = false diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 826b047c78..ceba580a57 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -7213,7 +7213,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { if accountTabBarAvatarBadge > 0 { otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } - return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge, accountTabBarAvatar != nil) + return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge, accountTabBarAvatar != nil || presentationData.reduceMotion) } self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue, isAvatar in diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 9292ecc789..3a5ca4785a 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -330,7 +330,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } if let dustNode = self.dustNode { - dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, textColor: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.secondaryTextColor, textColor: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } } else if let dustNode = self.dustNode { diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index 9d4fefba03..649ca55f98 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -348,8 +348,8 @@ public class ShareRootControllerImpl { } |> runOn(Queue.mainQueue()) } - let sentItems: ([PeerId], [PreparedShareItemContent], Account) -> Signal = { peerIds, contents, account in - let sentItems = sentShareItems(account: account, to: peerIds, items: contents) + let sentItems: ([PeerId], [PreparedShareItemContent], Account, Bool) -> Signal = { peerIds, contents, account, silently in + let sentItems = sentShareItems(account: account, to: peerIds, items: contents, silently: silently) |> `catch` { _ -> Signal< Float, NoError> in return .complete() @@ -361,7 +361,7 @@ public class ShareRootControllerImpl { |> then(.single(.done)) } - let shareController = ShareController(context: context, subject: .fromExternal({ peerIds, additionalText, account in + let shareController = ShareController(context: context, subject: .fromExternal({ peerIds, additionalText, account, silently in if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty { let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)! return preparedShareItems(account: account, to: peerIds[0], dataItems: rawSignals, additionalText: additionalText) @@ -381,10 +381,10 @@ public class ShareRootControllerImpl { case let .userInteractionRequired(value): return requestUserInteraction(value) |> mapToSignal { contents -> Signal in - return sentItems(peerIds, contents, account) + return sentItems(peerIds, contents, account, silently) } case let .done(contents): - return sentItems(peerIds, contents, account) + return sentItems(peerIds, contents, account, silently) } } } else { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 9f0ec95069..955e51f15b 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 9f0ec9506970fc4c09d0542716b64e643033bd76 +Subproject commit 955e51f15ba1b2e35a910064388e5c0737032bb7 diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h index 366137ba47..d74520c5d6 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h @@ -49,5 +49,6 @@ NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem * _Nonnull item, UITabBa - (void)removeSetSelectedImageListener:(NSInteger)key; @property (nonatomic, strong) NSString * _Nullable animationName; +@property (nonatomic, assign) CGPoint animationOffset; @end diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m index 99f6b435aa..bac5ab17db 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m @@ -17,6 +17,7 @@ static const void *setBackBarButtonItemListenerBagKey = &setBackBarButtonItemLis static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; static const void *badgeKey = &badgeKey; static const void *animationNameKey = &animationNameKey; +static const void *animationOffsetKey = &animationOffsetKey; @implementation UINavigationItem (Proxy) @@ -404,14 +405,18 @@ NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem *item, UITabBarItemSetBa - (void)setAnimationName:(NSString *)animationName { [self setAssociatedObject:animationName forKey:animationNameKey]; - -// [(NSBag *)[self associatedObjectForKey:setBadgeListenerBagKey] enumerateItems:^(UITabBarItemSetBadgeListener listener) { -// listener(badge); -// }]; } - (NSString *)animationName { return [self associatedObjectForKey:animationNameKey]; } +- (void)setAnimationOffset:(CGPoint)animationOffset { + [self setAssociatedObject:[NSValue valueWithCGPoint:animationOffset] forKey:animationOffsetKey]; +} + +- (CGPoint)animationOffset { + return ((NSValue *)[self associatedObjectForKey:animationOffsetKey]).CGPointValue; +} + @end diff --git a/third-party/webrtc/BUILD b/third-party/webrtc/BUILD index ef05676784..a7cb262c36 100644 --- a/third-party/webrtc/BUILD +++ b/third-party/webrtc/BUILD @@ -683,7 +683,6 @@ webrtc_sources = [ "api/video_codecs/video_decoder.cc", "api/video_codecs/video_decoder_software_fallback_wrapper.cc", "api/video_codecs/video_encoder.cc", - "api/video_codecs/video_encoder_config.cc", "api/video_codecs/video_encoder_software_fallback_wrapper.cc", "api/video_codecs/vp8_frame_config.cc", "api/video_codecs/vp8_temporal_layers.cc", @@ -1285,12 +1284,10 @@ webrtc_sources = [ "modules/video_coding/codecs/multiplex/multiplex_encoded_image_packer.cc", "modules/video_coding/codecs/multiplex/multiplex_encoder_adapter.cc", "modules/video_coding/decoder_database.cc", - "modules/video_coding/decoding_state.cc", "modules/video_coding/encoded_frame.cc", - "modules/video_coding/event_wrapper.cc", "modules/video_coding/fec_controller_default.cc", - "modules/video_coding/frame_buffer.cc", "modules/video_coding/frame_buffer2.cc", + "modules/video_coding/frame_buffer3.cc", "modules/video_coding/frame_dependencies_calculator.cc", "modules/video_coding/frame_object.cc", "modules/video_coding/generic_decoder.cc", @@ -1299,16 +1296,12 @@ webrtc_sources = [ "modules/video_coding/histogram.cc", "modules/video_coding/include/video_codec_interface.cc", "modules/video_coding/inter_frame_delay.cc", - "modules/video_coding/jitter_buffer.cc", "modules/video_coding/jitter_estimator.cc", "modules/video_coding/loss_notification_controller.cc", "modules/video_coding/media_opt_util.cc", - "modules/video_coding/packet.cc", "modules/video_coding/packet_buffer.cc", - "modules/video_coding/receiver.cc", "modules/video_coding/rtp_frame_reference_finder.cc", "modules/video_coding/rtt_filter.cc", - "modules/video_coding/session_info.cc", "modules/video_coding/timestamp_map.cc", "modules/video_coding/timing.cc", "modules/video_coding/unique_timestamp_counter.cc", @@ -1323,8 +1316,6 @@ webrtc_sources = [ "modules/video_coding/utility/vp9_uncompressed_header_parser.cc", "modules/video_coding/video_codec_initializer.cc", "modules/video_coding/video_coding_defines.cc", - "modules/video_coding/video_coding_impl.cc", - "modules/video_coding/video_receiver.cc", "modules/video_coding/video_receiver2.cc", "modules/video_coding/codecs/vp8/default_temporal_layers.cc", "modules/video_coding/codecs/vp8/libvpx_vp8_decoder.cc", @@ -1521,7 +1512,6 @@ webrtc_sources = [ "video/stream_synchronization.cc", "video/transport_adapter.cc", "video/video_quality_observer.cc", - "video/video_receive_stream.cc", "video/video_send_stream.cc", "video/video_send_stream_impl.cc", "video/video_source_sink_controller.cc", @@ -2464,7 +2454,6 @@ webrtc_sources = [ "video/stream_synchronization.h", "video/transport_adapter.h", "video/video_quality_observer.h", - "video/video_receive_stream.h", "video/video_send_stream_impl.h", "video/video_source_sink_controller.h", "video/video_stream_decoder_impl.h", @@ -2949,6 +2938,19 @@ webrtc_sources = [ "modules/video_coding/h265_vps_sps_pps_tracker.cc", "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc", "common_video/h265/h265_bitstream_parser.cc", + "api/video_codecs/video_encoder_config.cc", + "video/frame_cadence_adapter.cc", + "modules/video_coding/codecs/h265/include/h265_globals.h", + "video/frame_cadence_adapter.h", + "common_video/h265/h265_common.h", + "modules/video_coding/h265_vps_sps_pps_tracker.h", + "common_video/h265/h265_pps_parser.h", + "modules/video_coding/frame_buffer3.h", + "common_video/h265/h265_bitstream_parser.h", + "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h", + "common_video/h265/h265_sps_parser.h", + "modules/rtp_rtcp/source/rtp_format_h265.h", + "common_video/h265/h265_vps_parser.h", ] ios_objc_sources = [ @@ -3212,6 +3214,9 @@ ios_sources = [ "objc/native/src/network_monitor_observer.h", "objc/components/video_codec/RTCCodecSpecificInfoH265.mm", "objc/components/video_codec/RTCH265ProfileLevelId.mm", + "objc/components/video_codec/RTCCodecSpecificInfoH265+Private.h", + "objc/components/video_codec/RTCH265ProfileLevelId.h", + "objc/components/video_codec/RTCCodecSpecificInfoH265.h", ] common_arm_specific_sources = [webrtc_source_dir + "/" + path for path in [ diff --git a/third-party/webrtc/webrtc b/third-party/webrtc/webrtc index a3dbc94d93..4f09932cfd 160000 --- a/third-party/webrtc/webrtc +++ b/third-party/webrtc/webrtc @@ -1 +1 @@ -Subproject commit a3dbc94d93356bc10c2388a9291baf5a6105bdee +Subproject commit 4f09932cfdda8c8a432e8e5ace32c9ae840f6201