From 1c2f989fbd262cb0c2a0f3431d062e92d29e8d06 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 25 Apr 2023 18:04:51 +0400 Subject: [PATCH] Story improvements --- .../Sources/AccountContext.swift | 5 + .../Sources/ContactSelectionController.swift | 1 + .../Sources/PresentationCallManager.swift | 1 + .../Sources/AttachmentController.swift | 1 + .../Sources/Node/ChatListItem.swift | 16 +- submodules/ICloudResources/BUILD | 22 + .../Sources/ICloudResources.swift | 22 +- .../LegacyUI/Sources/LegacyController.swift | 4 + submodules/TelegramApi/Sources/Api0.swift | 16 +- submodules/TelegramApi/Sources/Api14.swift | 210 +-- submodules/TelegramApi/Sources/Api17.swift | 10 +- submodules/TelegramApi/Sources/Api21.swift | 20 +- submodules/TelegramApi/Sources/Api28.swift | 38 +- submodules/TelegramApi/Sources/Api30.swift | 19 - .../ApiUtils/ReactionsMessageAttribute.swift | 8 +- .../Sources/ApiUtils/TelegramMediaPoll.swift | 2 +- .../Sources/State/MessageReactions.swift | 15 +- .../Sources/State/PendingMessageManager.swift | 81 +- .../Sources/State/Serialization.swift | 2 +- .../SyncCore_ReactionsMessageAttribute.swift | 23 +- .../TelegramEngine/Messages/Polls.swift | 19 +- submodules/TelegramUI/BUILD | 3 + .../ChatScheduleTimeController/BUILD | 28 + .../Sources/ChatScheduleTimeController.swift | 10 +- .../ChatScheduleTimeControllerNode.swift | 0 .../TelegramUI/Components/LegacyCamera/BUILD | 28 + .../LegacyCamera}/Sources/LegacyCamera.swift | 4 +- .../MessageInputPanelComponent/BUILD | 1 + .../Sources/MessageInputPanelComponent.swift | 45 +- .../Stories/StoryContainerScreen/BUILD | 20 + .../MediaNavigationStripComponent.swift | 44 +- .../Sources/StoryContainerScreen.swift | 1585 ++++++++++++++++- .../Sources/StoryContent.swift | 34 +- .../Sources/StoryChatContent.swift | 3 +- .../StoryMessageContentComponent.swift | 45 +- .../Stories/StoryFooterPanelComponent/BUILD | 21 + .../Sources/StoryFooterPanelComponent.swift | 48 + .../Sources/AttachmentFileController.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 117 +- .../Sources/ContactSelectionController.swift | 2 +- .../Sources/DeclareEncodables.swift | 1 + .../Sources/SharedAccountContext.swift | 105 ++ .../TelegramAccountAuxiliaryMethods.swift | 1 + .../Sources/TelegramRootController.swift | 1 + 44 files changed, 2340 insertions(+), 345 deletions(-) create mode 100644 submodules/ICloudResources/BUILD rename submodules/{TelegramUI => ICloudResources}/Sources/ICloudResources.swift (94%) create mode 100644 submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD rename submodules/TelegramUI/{ => Components/ChatScheduleTimeController}/Sources/ChatScheduleTimeController.swift (87%) rename submodules/TelegramUI/{ => Components/ChatScheduleTimeController}/Sources/ChatScheduleTimeControllerNode.swift (100%) create mode 100644 submodules/TelegramUI/Components/LegacyCamera/BUILD rename submodules/TelegramUI/{ => Components/LegacyCamera}/Sources/LegacyCamera.swift (92%) create mode 100644 submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index abd301d1c9..ccbb93c16f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -738,6 +738,9 @@ public protocol AppLockContext: AnyObject { public protocol RecentSessionsController: AnyObject { } +public protocol AttachmentFileController: AnyObject { +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -805,6 +808,8 @@ public protocol SharedAccountContext: AnyObject { func makePrivacyAndSecurityController(context: AccountContext) -> ViewController func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController + func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController + func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index f13ca03a53..ac7907088c 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -6,6 +6,7 @@ public protocol ContactSelectionController: ViewController { var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } + var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void { get set } func dismissSearch() } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index b7d1b605de..ec71e5ff31 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -463,6 +463,7 @@ public protocol PresentationGroupCall: AnyObject { public protocol PresentationCallManager: AnyObject { var currentCallSignal: Signal { get } var currentGroupCallSignal: Signal { get } + var hasActiveCall: Bool { get } func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 8edb125876..fb64eed88a 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -84,6 +84,7 @@ public protocol AttachmentContainable: ViewController { var cancelPanGesture: () -> Void { get set } var isContainerPanning: () -> Bool { get set } var isContainerExpanded: () -> Bool { get set } + var mediaPickerContext: AttachmentMediaPickerContext? { get } func isContainerPanningUpdated(_ panning: Bool) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 89de56c420..17586f6241 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -611,6 +611,8 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } +private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) + class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { let topicTitleNode: TextNode @@ -1841,7 +1843,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) } - let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + var entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in switch entity.type { case .Spoiler, .CustomEmoji: return true @@ -1851,6 +1853,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { return false } } + if message.author?.id.id == PeerId.Id._internalFromInt64Value(777000) { + if let loginCodeRegex { + let results = loginCodeRegex.matches(in: message.text, range: NSRange(message.text.startIndex..., in: message.text)) + for result in results { + let spoilerRange: Range = result.range.location ..< (result.range.location + result.range.length) + if !entities.contains(where: { $0.range.overlaps(spoilerRange) }) { + entities.append(MessageTextEntity(range: spoilerRange, type: .Spoiler)) + } + } + } + } + let messageString: NSAttributedString if !message.text.isEmpty && entities.count > 0 { var messageText = message.text diff --git a/submodules/ICloudResources/BUILD b/submodules/ICloudResources/BUILD new file mode 100644 index 0000000000..b282708cee --- /dev/null +++ b/submodules/ICloudResources/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ICloudResources", + module_name = "ICloudResources", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/Pdf", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ICloudResources.swift b/submodules/ICloudResources/Sources/ICloudResources.swift similarity index 94% rename from submodules/TelegramUI/Sources/ICloudResources.swift rename to submodules/ICloudResources/Sources/ICloudResources.swift index 88b097aacd..c24cb17a46 100644 --- a/submodules/TelegramUI/Sources/ICloudResources.swift +++ b/submodules/ICloudResources/Sources/ICloudResources.swift @@ -63,17 +63,17 @@ public class ICloudFileResource: TelegramMediaResource { } } -struct ICloudFileDescription { - struct AudioMetadata { - let title: String? - let performer: String? - let duration: Int +public struct ICloudFileDescription { + public struct AudioMetadata { + public let title: String? + public let performer: String? + public let duration: Int } - let urlData: String - let fileName: String - let fileSize: Int - let audioMetadata: AudioMetadata? + public let urlData: String + public let fileName: String + public let fileSize: Int + public let audioMetadata: AudioMetadata? } private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? { @@ -115,7 +115,7 @@ private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? { } } -func iCloudFileDescription(_ url: URL) -> Signal { +public func iCloudFileDescription(_ url: URL) -> Signal { return Signal { subscriber in var isRemote = false var isCurrent = true @@ -191,7 +191,7 @@ private final class ICloudFileResourceCopyItem: MediaResourceDataFetchCopyLocalI } } -func fetchICloudFileResource(resource: ICloudFileResource) -> Signal { +public func fetchICloudFileResource(resource: ICloudFileResource) -> Signal { return Signal { subscriber in subscriber.putNext(.reset) diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index d40dec056e..2723f14451 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -438,6 +438,10 @@ open class LegacyController: ViewController, PresentableController, AttachmentCo open var isContainerPanning: () -> Bool = { return false } open var isContainerExpanded: () -> Bool = { return false } + public var mediaPickerContext: AttachmentMediaPickerContext? { + return nil + } + public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { self.sizeClass.set(SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber)) self.presentation = presentation diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index bd9372bcd0..4b92fdfbe0 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -524,13 +524,13 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) } dict[-1557277184] = { return Api.MessageMedia.parse_messageMediaWebPage($0) } dict[-1938180548] = { return Api.MessagePeerReaction.parse_messagePeerReaction($0) } + dict[-1228133028] = { return Api.MessagePeerVote.parse_messagePeerVote($0) } + dict[1959634180] = { return Api.MessagePeerVote.parse_messagePeerVoteInputOption($0) } + dict[1177089766] = { return Api.MessagePeerVote.parse_messagePeerVoteMultiple($0) } dict[182649427] = { return Api.MessageRange.parse_messageRange($0) } dict[1328256121] = { return Api.MessageReactions.parse_messageReactions($0) } dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) } dict[-1495959709] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } - dict[886196148] = { return Api.MessageUserVote.parse_messageUserVote($0) } - dict[1017491692] = { return Api.MessageUserVote.parse_messageUserVoteInputOption($0) } - dict[-1973033641] = { return Api.MessageUserVote.parse_messageUserVoteMultiple($0) } dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) } dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) } dict[-530392189] = { return Api.MessagesFilter.parse_inputMessagesFilterContacts($0) } @@ -635,7 +635,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2032041631] = { return Api.Poll.parse_poll($0) } dict[1823064809] = { return Api.PollAnswer.parse_pollAnswer($0) } dict[997055186] = { return Api.PollAnswerVoters.parse_pollAnswerVoters($0) } - dict[-591909213] = { return Api.PollResults.parse_pollResults($0) } + dict[2061444128] = { return Api.PollResults.parse_pollResults($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[512535275] = { return Api.PostAddress.parse_postAddress($0) } dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) } @@ -856,7 +856,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1517529484] = { return Api.Update.parse_updateMessageExtendedMedia($0) } dict[1318109142] = { return Api.Update.parse_updateMessageID($0) } dict[-1398708869] = { return Api.Update.parse_updateMessagePoll($0) } - dict[274961865] = { return Api.Update.parse_updateMessagePollVote($0) } + dict[619974263] = { return Api.Update.parse_updateMessagePollVote($0) } dict[1578843320] = { return Api.Update.parse_updateMessageReactions($0) } dict[-2030252155] = { return Api.Update.parse_updateMoveStickerSetToTop($0) } dict[1656358105] = { return Api.Update.parse_updateNewChannelMessage($0) } @@ -1108,7 +1108,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) } dict[-1821037486] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) } dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) } - dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } + dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) } @@ -1522,6 +1522,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessagePeerReaction: _1.serialize(buffer, boxed) + case let _1 as Api.MessagePeerVote: + _1.serialize(buffer, boxed) case let _1 as Api.MessageRange: _1.serialize(buffer, boxed) case let _1 as Api.MessageReactions: @@ -1530,8 +1532,6 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageReplyHeader: _1.serialize(buffer, boxed) - case let _1 as Api.MessageUserVote: - _1.serialize(buffer, boxed) case let _1 as Api.MessageViews: _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 5f7b972e35..7b2668a632 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -50,6 +50,114 @@ public extension Api { } } +public extension Api { + enum MessagePeerVote: TypeConstructorDescription { + case messagePeerVote(peer: Api.Peer, option: Buffer, date: Int32) + case messagePeerVoteInputOption(peer: Api.Peer, date: Int32) + case messagePeerVoteMultiple(peer: Api.Peer, options: [Buffer], date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messagePeerVote(let peer, let option, let date): + if boxed { + buffer.appendInt32(-1228133028) + } + peer.serialize(buffer, true) + serializeBytes(option, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + case .messagePeerVoteInputOption(let peer, let date): + if boxed { + buffer.appendInt32(1959634180) + } + peer.serialize(buffer, true) + serializeInt32(date, buffer: buffer, boxed: false) + break + case .messagePeerVoteMultiple(let peer, let options, let date): + if boxed { + buffer.appendInt32(1177089766) + } + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(options.count)) + for item in options { + serializeBytes(item, buffer: buffer, boxed: false) + } + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messagePeerVote(let peer, let option, let date): + return ("messagePeerVote", [("peer", peer as Any), ("option", option as Any), ("date", date as Any)]) + case .messagePeerVoteInputOption(let peer, let date): + return ("messagePeerVoteInputOption", [("peer", peer as Any), ("date", date as Any)]) + case .messagePeerVoteMultiple(let peer, let options, let date): + return ("messagePeerVoteMultiple", [("peer", peer as Any), ("options", options as Any), ("date", date as Any)]) + } + } + + public static func parse_messagePeerVote(_ reader: BufferReader) -> MessagePeerVote? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessagePeerVote.messagePeerVote(peer: _1!, option: _2!, date: _3!) + } + else { + return nil + } + } + public static func parse_messagePeerVoteInputOption(_ reader: BufferReader) -> MessagePeerVote? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessagePeerVote.messagePeerVoteInputOption(peer: _1!, date: _2!) + } + else { + return nil + } + } + public static func parse_messagePeerVoteMultiple(_ reader: BufferReader) -> MessagePeerVote? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: [Buffer]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessagePeerVote.messagePeerVoteMultiple(peer: _1!, options: _2!, date: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum MessageRange: TypeConstructorDescription { case messageRange(minId: Int32, maxId: Int32) @@ -262,108 +370,6 @@ public extension Api { } } -public extension Api { - enum MessageUserVote: TypeConstructorDescription { - case messageUserVote(userId: Int64, option: Buffer, date: Int32) - case messageUserVoteInputOption(userId: Int64, date: Int32) - case messageUserVoteMultiple(userId: Int64, options: [Buffer], date: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageUserVote(let userId, let option, let date): - if boxed { - buffer.appendInt32(886196148) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeBytes(option, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - break - case .messageUserVoteInputOption(let userId, let date): - if boxed { - buffer.appendInt32(1017491692) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - break - case .messageUserVoteMultiple(let userId, let options, let date): - if boxed { - buffer.appendInt32(-1973033641) - } - serializeInt64(userId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(options.count)) - for item in options { - serializeBytes(item, buffer: buffer, boxed: false) - } - serializeInt32(date, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageUserVote(let userId, let option, let date): - return ("messageUserVote", [("userId", userId as Any), ("option", option as Any), ("date", date as Any)]) - case .messageUserVoteInputOption(let userId, let date): - return ("messageUserVoteInputOption", [("userId", userId as Any), ("date", date as Any)]) - case .messageUserVoteMultiple(let userId, let options, let date): - return ("messageUserVoteMultiple", [("userId", userId as Any), ("options", options as Any), ("date", date as Any)]) - } - } - - public static func parse_messageUserVote(_ reader: BufferReader) -> MessageUserVote? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Buffer? - _2 = parseBytes(reader) - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.MessageUserVote.messageUserVote(userId: _1!, option: _2!, date: _3!) - } - else { - return nil - } - } - public static func parse_messageUserVoteInputOption(_ reader: BufferReader) -> MessageUserVote? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MessageUserVote.messageUserVoteInputOption(userId: _1!, date: _2!) - } - else { - return nil - } - } - public static func parse_messageUserVoteMultiple(_ reader: BufferReader) -> MessageUserVote? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Buffer]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) - } - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.MessageUserVote.messageUserVoteMultiple(userId: _1!, options: _2!, date: _3!) - } - else { - return nil - } - } - - } -} public extension Api { enum MessageViews: TypeConstructorDescription { case messageViews(flags: Int32, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?) diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index f78c8d5144..4e135a387e 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -428,13 +428,13 @@ public extension Api { } public extension Api { enum PollResults: TypeConstructorDescription { - case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int64]?, solution: String?, solutionEntities: [Api.MessageEntity]?) + case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Api.Peer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities): if boxed { - buffer.appendInt32(-591909213) + buffer.appendInt32(2061444128) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) @@ -446,7 +446,7 @@ public extension Api { if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(recentVoters!.count)) for item in recentVoters! { - serializeInt64(item, buffer: buffer, boxed: false) + item.serialize(buffer, true) }} if Int(flags) & Int(1 << 4) != 0 {serializeString(solution!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) @@ -474,9 +474,9 @@ public extension Api { } } var _3: Int32? if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } - var _4: [Int64]? + var _4: [Api.Peer]? if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } } var _5: String? if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 24ec1c7915..bce9ec2791 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -785,7 +785,7 @@ public extension Api { case updateMessageExtendedMedia(peer: Api.Peer, msgId: Int32, extendedMedia: Api.MessageExtendedMedia) case updateMessageID(id: Int32, randomId: Int64) case updateMessagePoll(flags: Int32, pollId: Int64, poll: Api.Poll?, results: Api.PollResults) - case updateMessagePollVote(pollId: Int64, userId: Int64, options: [Buffer], qts: Int32) + case updateMessagePollVote(pollId: Int64, peer: Api.Peer, options: [Buffer], qts: Int32) case updateMessageReactions(flags: Int32, peer: Api.Peer, msgId: Int32, topMsgId: Int32?, reactions: Api.MessageReactions) case updateMoveStickerSetToTop(flags: Int32, stickerset: Int64) case updateNewChannelMessage(message: Api.Message, pts: Int32, ptsCount: Int32) @@ -1385,12 +1385,12 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {poll!.serialize(buffer, true)} results.serialize(buffer, true) break - case .updateMessagePollVote(let pollId, let userId, let options, let qts): + case .updateMessagePollVote(let pollId, let peer, let options, let qts): if boxed { - buffer.appendInt32(274961865) + buffer.appendInt32(619974263) } serializeInt64(pollId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) + peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(options.count)) for item in options { @@ -1923,8 +1923,8 @@ public extension Api { return ("updateMessageID", [("id", id as Any), ("randomId", randomId as Any)]) case .updateMessagePoll(let flags, let pollId, let poll, let results): return ("updateMessagePoll", [("flags", flags as Any), ("pollId", pollId as Any), ("poll", poll as Any), ("results", results as Any)]) - case .updateMessagePollVote(let pollId, let userId, let options, let qts): - return ("updateMessagePollVote", [("pollId", pollId as Any), ("userId", userId as Any), ("options", options as Any), ("qts", qts as Any)]) + case .updateMessagePollVote(let pollId, let peer, let options, let qts): + return ("updateMessagePollVote", [("pollId", pollId as Any), ("peer", peer as Any), ("options", options as Any), ("qts", qts as Any)]) case .updateMessageReactions(let flags, let peer, let msgId, let topMsgId, let reactions): return ("updateMessageReactions", [("flags", flags as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("topMsgId", topMsgId as Any), ("reactions", reactions as Any)]) case .updateMoveStickerSetToTop(let flags, let stickerset): @@ -3171,8 +3171,10 @@ public extension Api { public static func parse_updateMessagePollVote(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } var _3: [Buffer]? if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) @@ -3184,7 +3186,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.Update.updateMessagePollVote(pollId: _1!, userId: _2!, options: _3!, qts: _4!) + return Api.Update.updateMessagePollVote(pollId: _1!, peer: _2!, options: _3!, qts: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index c3cf009aea..e1794c8206 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -198,13 +198,13 @@ public extension Api.messages { } public extension Api.messages { enum VotesList: TypeConstructorDescription { - case votesList(flags: Int32, count: Int32, votes: [Api.MessageUserVote], users: [Api.User], nextOffset: String?) + case votesList(flags: Int32, count: Int32, votes: [Api.MessagePeerVote], chats: [Api.Chat], users: [Api.User], nextOffset: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .votesList(let flags, let count, let votes, let users, let nextOffset): + case .votesList(let flags, let count, let votes, let chats, let users, let nextOffset): if boxed { - buffer.appendInt32(136574537) + buffer.appendInt32(1218005070) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) @@ -214,6 +214,11 @@ public extension Api.messages { item.serialize(buffer, true) } buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) for item in users { item.serialize(buffer, true) @@ -225,8 +230,8 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .votesList(let flags, let count, let votes, let users, let nextOffset): - return ("votesList", [("flags", flags as Any), ("count", count as Any), ("votes", votes as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + case .votesList(let flags, let count, let votes, let chats, let users, let nextOffset): + return ("votesList", [("flags", flags as Any), ("count", count as Any), ("votes", votes as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } @@ -235,23 +240,28 @@ public extension Api.messages { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: [Api.MessageUserVote]? + var _3: [Api.MessagePeerVote]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageUserVote.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerVote.self) } - var _4: [Api.User]? + var _4: [Api.Chat]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _5: String? - if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.VotesList.votesList(flags: _1!, count: _2!, votes: _3!, users: _4!, nextOffset: _5) + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.VotesList.votesList(flags: _1!, count: _2!, votes: _3!, chats: _4!, users: _5!, nextOffset: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 66622b9ad0..474d3ccacf 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -4546,25 +4546,6 @@ public extension Api.functions.messages { }) } } -public extension Api.functions.messages { - static func getAllChats(exceptIds: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-2023787330) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(exceptIds.count)) - for item in exceptIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - return (FunctionDescription(name: "messages.getAllChats", parameters: [("exceptIds", String(describing: exceptIds))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in - let reader = BufferReader(buffer) - var result: Api.messages.Chats? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.messages.Chats - } - return result - }) - } -} public extension Api.functions.messages { static func getAllDrafts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index d2300cd98f..e54dd99a12 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -114,10 +114,14 @@ private func mergeReactions(reactions: [MessageReaction], recentPeers: [Reaction pendingIndex += 1 } - if let index = recentPeers.firstIndex(where: { $0.value == pendingReaction.value && $0.peerId == accountPeerId }) { + let pendingReactionSendAsPeerId = pendingReaction.sendAsPeerId ?? accountPeerId + + if let index = recentPeers.firstIndex(where: { + $0.value == pendingReaction.value && $0.peerId == pendingReactionSendAsPeerId + }) { recentPeers.remove(at: index) } - recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: pendingReaction.value, isLarge: false, isUnseen: false, peerId: accountPeerId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970))) + recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: pendingReaction.value, isLarge: false, isUnseen: false, peerId: pendingReactionSendAsPeerId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970))) } for i in (0 ..< result.count).reversed() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift index dfa545f2cd..3d58b37739 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift @@ -35,7 +35,7 @@ extension TelegramMediaPollResults { } self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in - return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } + return recentVoters.map { $0.peerId } } ?? [], solution: parsedSolution) } } diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 5a0dabd7f8..8e9fcc62b4 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -20,6 +20,15 @@ public enum UpdateMessageReaction { public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reactions: [UpdateMessageReaction], isLarge: Bool, storeAsRecentlyUsed: Bool) -> Signal { return account.postbox.transaction { transaction -> Void in + var sendAsPeerId = account.peerId + if let cachedData = transaction.getPeerCachedData(peerId: messageId.peerId) { + if let cachedData = cachedData as? CachedChannelData { + if let sendAsPeerIdValue = cachedData.sendAsPeerId { + sendAsPeerId = sendAsPeerIdValue + } + } + } + let isPremium = (transaction.getPeer(account.peerId) as? TelegramUser)?.isPremium ?? false let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue let maxCount: Int @@ -34,12 +43,12 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes for reaction in reactions { switch reaction { case let .custom(fileId, file): - mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .custom(fileId))) + mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .custom(fileId), sendAsPeerId: sendAsPeerId)) if let file = file { transaction.storeMediaIfNotPresent(media: file) } case let .builtin(value): - mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .builtin(value))) + mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .builtin(value), sendAsPeerId: sendAsPeerId)) } } @@ -88,7 +97,7 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes if updatedOutgoingReactions.count > maxCount { let sortedOutgoingReactions = updatedOutgoingReactions.sorted(by: { $0.chosenOrder! < $1.chosenOrder! }) mappedReactions = Array(sortedOutgoingReactions.suffix(maxCount).map { reaction -> PendingReactionsMessageAttribute.PendingReaction in - return PendingReactionsMessageAttribute.PendingReaction(value: reaction.value) + return PendingReactionsMessageAttribute.PendingReaction(value: reaction.value, sendAsPeerId: sendAsPeerId) }) } diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index efbebd2ee3..097328eb4d 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -446,20 +446,77 @@ public final class PendingMessageManager { Logger.shared.log("PendingMessageManager", "beginSendingMessages messagesToForward.count: \(messagesToForward.count)") - - for (_, messages) in messagesToForward { - for (context, _, _) in messages { - context.state = .sending(groupId: nil) + let forwardGroupLimit = 100 + for (_, ungroupedMessages) in messagesToForward { + var messageGroups: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]] = [] + + for message in ungroupedMessages { + if messageGroups.isEmpty || messageGroups[messageGroups.count - 1].isEmpty { + messageGroups.append([message]) + } else { + if messageGroups[messageGroups.count - 1][0].1.groupingKey == message.1.groupingKey { + messageGroups[messageGroups.count - 1].append(message) + } else { + messageGroups.append([message]) + } + } } - let sendMessage: Signal = strongSelf.sendGroupMessagesContent(network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, accountPeerId: strongSelf.accountPeerId, group: messages.map { data in - let (_, message, forwardInfo) = data - return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) - }) - |> map { next -> PendingMessageResult in - return .progress(1.0) + + var countedMessageGroups: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]] = [] + while !messageGroups.isEmpty { + guard let messageGroup = messageGroups.first else { + break + } + + messageGroups.removeFirst() + + if messageGroup.isEmpty { + continue + } + if countedMessageGroups.isEmpty { + countedMessageGroups.append([]) + } else if countedMessageGroups[countedMessageGroups.count - 1].count >= forwardGroupLimit { + countedMessageGroups.append([]) + } + + if countedMessageGroups[countedMessageGroups.count - 1].isEmpty { + let fittingFreeMessageCount = min(forwardGroupLimit, messageGroup.count) + countedMessageGroups[countedMessageGroups.count - 1].append(contentsOf: messageGroup[0 ..< fittingFreeMessageCount]) + if fittingFreeMessageCount < messageGroup.count { + messageGroups.insert(Array(messageGroup[fittingFreeMessageCount ..< messageGroup.count]), at: 0) + } + } else if countedMessageGroups[countedMessageGroups.count - 1].count + messageGroup.count <= forwardGroupLimit { + countedMessageGroups[countedMessageGroups.count - 1].append(contentsOf: messageGroup) + } else { + if countedMessageGroups[countedMessageGroups.count - 1][0].1.groupingKey == nil && messageGroup[0].1.groupingKey == nil { + let fittingFreeMessageCount = forwardGroupLimit - countedMessageGroups[countedMessageGroups.count - 1].count + countedMessageGroups[countedMessageGroups.count - 1].append(contentsOf: messageGroup[0 ..< fittingFreeMessageCount]) + messageGroups.insert(Array(messageGroup[fittingFreeMessageCount ..< messageGroup.count]), at: 0) + } else { + countedMessageGroups.append([]) + } + } + } + + for messages in countedMessageGroups { + if messages.isEmpty { + continue + } + + for (context, _, _) in messages { + context.state = .sending(groupId: nil) + } + + let sendMessage: Signal = strongSelf.sendGroupMessagesContent(network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, accountPeerId: strongSelf.accountPeerId, group: messages.map { data in + let (_, message, forwardInfo) = data + return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) + }) + |> map { next -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(strongSelf.queue)).start()) } - messages[0].0.sendDisposable.set((sendMessage - |> deliverOn(strongSelf.queue)).start()) } } })) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 6a9cc74562..72656f0174 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 158 + return 159 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index ed23a4220a..e8fcb4bf9e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -241,17 +241,25 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { public final class PendingReactionsMessageAttribute: MessageAttribute { public struct PendingReaction: Equatable, PostboxCoding { public var value: MessageReaction.Reaction + public var sendAsPeerId: PeerId? - public init(value: MessageReaction.Reaction) { + public init(value: MessageReaction.Reaction, sendAsPeerId: PeerId?) { self.value = value + self.sendAsPeerId = sendAsPeerId } public init(decoder: PostboxDecoder) { self.value = decoder.decodeObjectForKey("val", decoder: { MessageReaction.Reaction(decoder: $0) }) as! MessageReaction.Reaction + self.sendAsPeerId = decoder.decodeOptionalInt64ForKey("sa").flatMap(PeerId.init) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.value, forKey: "val") + if let sendAsPeerId = self.sendAsPeerId { + encoder.encodeInt64(sendAsPeerId.toInt64(), forKey: "sa") + } else { + encoder.encodeNil(forKey: "sa") + } } } @@ -261,11 +269,18 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { public let storeAsRecentlyUsed: Bool public var associatedPeerIds: [PeerId] { + var peerIds: [PeerId] = [] if let accountPeerId = self.accountPeerId { - return [accountPeerId] - } else { - return [] + peerIds.append(accountPeerId) } + for reaction in self.reactions { + if let sendAsPeerId = reaction.sendAsPeerId { + if !peerIds.contains(sendAsPeerId) { + peerIds.append(sendAsPeerId) + } + } + } + return peerIds } public var associatedMediaIds: [MediaId] { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index bfe5d5ab8c..7786e3a495 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -272,8 +272,13 @@ private final class PollResultsOptionContext { return ([], 0, nil) } switch result { - case let .votesList(_, count, votes, users, nextOffset): + case let .votesList(_, count, votes, chats, users, nextOffset): var peers: [Peer] = [] + for apiChat in chats { + if let peer = parseTelegramGroupOrChannel(chat: apiChat) { + peers.append(peer) + } + } for apiUser in users { peers.append(TelegramUser(user: apiUser)) } @@ -284,12 +289,12 @@ private final class PollResultsOptionContext { for vote in votes { let peerId: PeerId switch vote { - case let .messageUserVote(userId, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .messageUserVoteInputOption(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .messageUserVoteMultiple(userId, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .messagePeerVote(peerIdValue, _, _): + peerId = peerIdValue.peerId + case let .messagePeerVoteInputOption(peerIdValue, _): + peerId = peerIdValue.peerId + case let .messagePeerVoteMultiple(peerIdValue, _, _): + peerId = peerIdValue.peerId } if let peer = transaction.getPeer(peerId) { resultPeers.append(RenderedPeer(peer: peer)) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 7b5ead0a17..82b7dfd923 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -360,6 +360,9 @@ swift_library( "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/Stories/StoryContentComponent", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", + "//submodules/ICloudResources", + "//submodules/TelegramUI/Components/LegacyCamera", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD b/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD new file mode 100644 index 0000000000..f1c9f807de --- /dev/null +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatScheduleTimeController", + module_name = "ChatScheduleTimeController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramStringFormatting", + "//submodules/SolidRoundedButtonNode", + "//submodules/PresentationDataUtils", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift similarity index 87% rename from submodules/TelegramUI/Sources/ChatScheduleTimeController.swift rename to submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift index e3dd91b81b..7e8a2c5462 100644 --- a/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift @@ -8,17 +8,17 @@ import SwiftSignalKit import AccountContext import TelegramPresentationData -enum ChatScheduleTimeControllerMode { +public enum ChatScheduleTimeControllerMode { case scheduledMessages(sendWhenOnlineAvailable: Bool) case reminders } -enum ChatScheduleTimeControllerStyle { +public enum ChatScheduleTimeControllerStyle { case `default` case media } -final class ChatScheduleTimeController: ViewController { +public final class ChatScheduleTimeController: ViewController { private var controllerNode: ChatScheduleTimeControllerNode { return self.displayNode as! ChatScheduleTimeControllerNode } @@ -37,7 +37,7 @@ final class ChatScheduleTimeController: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, mode: ChatScheduleTimeControllerMode, style: ChatScheduleTimeControllerStyle, currentTime: Int32? = nil, minimalTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, mode: ChatScheduleTimeControllerMode, style: ChatScheduleTimeControllerStyle, currentTime: Int32? = nil, minimalTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { self.context = context self.peerId = peerId self.mode = mode @@ -66,7 +66,7 @@ final class ChatScheduleTimeController: ViewController { self.statusBar.statusBarStyle = .Ignore } - required init(coder aDecoder: NSCoder) { + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift rename to submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift diff --git a/submodules/TelegramUI/Components/LegacyCamera/BUILD b/submodules/TelegramUI/Components/LegacyCamera/BUILD new file mode 100644 index 0000000000..b9cc01c728 --- /dev/null +++ b/submodules/TelegramUI/Components/LegacyCamera/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LegacyCamera", + module_name = "LegacyCamera", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/LegacyComponents", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/AccountContext", + "//submodules/ShareController", + "//submodules/LegacyUI", + "//submodules/LegacyMediaPickerUI", + + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift similarity index 92% rename from submodules/TelegramUI/Sources/LegacyCamera.swift rename to submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 41a2e9f810..efcdbc69b1 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -202,7 +202,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch parentController.present(legacyController, in: .window(.root)) } -func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedMedia: Bool, saveEditedPhotos: Bool, mediaGrouping: Bool, parentController: ViewController) { +public func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedMedia: Bool, saveEditedPhotos: Bool, mediaGrouping: Bool, parentController: ViewController) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index c7f54f14a7..792140ceab 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/AppBundle", "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/Components/BundleIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 2117b8aed1..4297831d39 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import AppBundle import TextFieldComponent +import BundleIconComponent public final class MessageInputPanelComponent: Component { public final class ExternalState { @@ -15,13 +16,16 @@ public final class MessageInputPanelComponent: Component { public let externalState: ExternalState public let sendMessageAction: () -> Void + public let attachmentAction: () -> Void public init( externalState: ExternalState, - sendMessageAction: @escaping () -> Void + sendMessageAction: @escaping () -> Void, + attachmentAction: @escaping () -> Void ) { self.externalState = externalState self.sendMessageAction = sendMessageAction + self.attachmentAction = attachmentAction } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -41,7 +45,7 @@ public final class MessageInputPanelComponent: Component { private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() - private let attachmentIconView: UIImageView + private let attachmentButton = ComponentView() private let inputActionButton = ComponentView() private let stickerIconView: UIImageView @@ -52,7 +56,6 @@ public final class MessageInputPanelComponent: Component { override init(frame: CGRect) { self.fieldBackgroundView = UIImageView() - self.attachmentIconView = UIImageView() self.stickerIconView = UIImageView() super.init(frame: frame) @@ -60,7 +63,6 @@ public final class MessageInputPanelComponent: Component { self.addSubview(self.fieldBackgroundView) self.addSubview(self.fieldBackgroundView) - self.addSubview(self.attachmentIconView) self.addSubview(self.stickerIconView) } @@ -76,6 +78,13 @@ public final class MessageInputPanelComponent: Component { return .text(textFieldView.getText()) } + public func getAttachmentButtonView() -> UIView? { + guard let attachmentButtonView = self.attachmentButton.view else { + return nil + } + return attachmentButtonView + } + public func clearSendMessageInput() { if let textFieldView = self.textField.view as? TextFieldComponent.View { textFieldView.setText(string: "") @@ -93,10 +102,6 @@ public final class MessageInputPanelComponent: Component { if self.fieldBackgroundView.image == nil { self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil) } - if self.attachmentIconView.image == nil { - self.attachmentIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.withRenderingMode(.alwaysTemplate) - self.attachmentIconView.tintColor = .white - } if self.stickerIconView.image == nil { self.stickerIconView.image = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.withRenderingMode(.alwaysTemplate) self.stickerIconView.tintColor = .white @@ -129,8 +134,28 @@ public final class MessageInputPanelComponent: Component { transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldFrame.minX, y: fieldFrame.maxY - textFieldSize.height), size: textFieldSize)) } - if let image = self.attachmentIconView.image { - transition.setFrame(view: self.attachmentIconView, frame: CGRect(origin: CGPoint(x: floor((insets.left - image.size.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - image.size.height) * 0.5)), size: image.size)) + let attachmentButtonSize = self.attachmentButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Text/IconAttachment", + tintColor: .white + )), + action: { [weak self] in + guard let self else { + return + } + self.component?.attachmentAction() + } + ).minSize(CGSize(width: 41.0, height: baseHeight))), + environment: {}, + containerSize: CGSize(width: 41.0, height: baseHeight) + ) + if let attachmentButtonView = self.attachmentButton.view { + if attachmentButtonView.superview == nil { + self.addSubview(attachmentButtonView) + } + transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize)) } let inputActionButtonSize = self.inputActionButton.update( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 039f18159f..33840888b0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -15,12 +15,32 @@ swift_library( "//submodules/Components/ViewControllerComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/AccountContext", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AppBundle", "//submodules/TelegramCore", "//submodules/ShareController", "//submodules/UndoUI", + "//submodules/AttachmentUI", + "//submodules/TelegramUIPreferences", + "//submodules/MediaPickerUI", + "//submodules/LegacyMediaPickerUI", + "//submodules/LocationUI", + "//submodules/WebUI", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", + "//submodules/TelegramUI/Components/ChatTimerScreen", + "//submodules/TextFormat", + "//submodules/PhoneNumberFormat", + "//submodules/ComposePollUI", + "//submodules/TelegramIntents", + "//submodules/LegacyUI", + "//submodules/WebSearchUI", + "//submodules/PremiumUI", + "//submodules/ICloudResources", + "//submodules/LegacyComponents", + "//submodules/TelegramUI/Components/LegacyCamera", + "//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift index 8de025765b..353d3cb700 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift @@ -4,6 +4,21 @@ import Display import ComponentFlow final class MediaNavigationStripComponent: Component { + final class EnvironmentType: Equatable { + let currentProgress: Double + + init(currentProgress: Double) { + self.currentProgress = currentProgress + } + + static func ==(lhs: EnvironmentType, rhs: EnvironmentType) -> Bool { + if lhs.currentProgress != rhs.currentProgress { + return false + } + return true + } + } + let index: Int let count: Int @@ -23,10 +38,17 @@ final class MediaNavigationStripComponent: Component { } private final class ItemLayer: SimpleLayer { + let foregroundLayer: SimpleLayer + override init() { + self.foregroundLayer = SimpleLayer() + super.init() self.cornerRadius = 1.5 + + self.foregroundLayer.cornerRadius = 1.5 + self.addSublayer(self.foregroundLayer) } required init?(coder: NSCoder) { @@ -34,6 +56,8 @@ final class MediaNavigationStripComponent: Component { } override init(layer: Any) { + self.foregroundLayer = SimpleLayer() + super.init(layer: layer) } } @@ -52,10 +76,10 @@ final class MediaNavigationStripComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let spacing: CGFloat = 3.0 let itemHeight: CGFloat = 2.0 - let minItemWidth: CGFloat = 3.0 + let minItemWidth: CGFloat = 10.0 var validIndices: [Int] = [] if component.count != 0 { @@ -110,7 +134,19 @@ final class MediaNavigationStripComponent: Component { transition.setFrame(layer: itemLayer, frame: itemFrame) - itemLayer.backgroundColor = UIColor(white: 1.0, alpha: i == component.index ? 1.0 : 0.5).cgColor + itemLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor + itemLayer.foregroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 1.0).cgColor + + let itemProgress: CGFloat + if i < component.index { + itemProgress = 1.0 + } else if i == component.index { + itemProgress = max(0.0, min(1.0, environment[EnvironmentType.self].value.currentProgress)) + } else { + itemProgress = 0.0 + } + + transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: itemProgress * itemFrame.width, height: itemFrame.height))) } } @@ -133,7 +169,7 @@ final class MediaNavigationStripComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index e5e3f1d83a..1d994d0eb5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -10,6 +10,26 @@ import MessageInputPanelComponent import ShareController import TelegramCore import UndoUI +import AttachmentUI +import TelegramUIPreferences +import MediaPickerUI +import LegacyMediaPickerUI +import LocationUI +import ChatEntityKeyboardInputNode +import WebUI +import ChatScheduleTimeController +import TextFormat +import PhoneNumberFormat +import ComposePollUI +import TelegramIntents +import LegacyUI +import WebSearchUI +import ChatTimerScreen +import PremiumUI +import ICloudResources +import LegacyComponents +import LegacyCamera +import StoryFooterPanelComponent private func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { @@ -66,7 +86,10 @@ private final class StoryContainerScreenComponent: Component { } private final class VisibleItem { - let view = ComponentView() + let externalState = StoryContentItem.ExternalState() + let view = ComponentView() + var currentProgress: Double = 0.0 + var requestedNext: Bool = false init() { } @@ -92,15 +115,20 @@ private final class StoryContainerScreenComponent: Component { private let closeButton: HighlightableButton private let closeButtonIconView: UIImageView - private let navigationStrip = ComponentView() + private let navigationStrip = ComponentView() private let inlineActions = ComponentView() private var centerInfoItem: InfoItem? private var rightInfoItem: InfoItem? private let inputPanel = ComponentView() + private let footerPanel = ComponentView() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() + private weak var attachmentController: AttachmentController? + private let controllerNavigationDisposable = MetaDisposable() + private let enqueueMediaMessageDisposable = MetaDisposable() + private var component: StoryContainerScreenComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? @@ -169,6 +197,8 @@ private final class StoryContainerScreenComponent: Component { deinit { self.currentSliceDisposable?.dispose() + self.controllerNavigationDisposable.dispose() + self.enqueueMediaMessageDisposable.dispose() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @@ -249,7 +279,57 @@ private final class StoryContainerScreenComponent: Component { let _ = visibleItem.view.update( transition: itemTransition, component: focusedItem.component, - environment: {}, + environment: { + StoryContentItem.Environment( + externalState: visibleItem.externalState, + presentationProgressUpdated: { [weak self, weak visibleItem] progress in + guard let self = self else { + return + } + guard let visibleItem else { + return + } + visibleItem.currentProgress = progress + + let _ = self.navigationStrip.updateEnvironment( + transition: .immediate, + environment: { + MediaNavigationStripComponent.EnvironmentType( + currentProgress: progress + ) + } + ) + if progress >= 1.0 && !visibleItem.requestedNext { + visibleItem.requestedNext = true + + if let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) { + var nextIndex = currentIndex - 1 + nextIndex = max(0, min(nextIndex, currentSlice.items.count - 1)) + if nextIndex != currentIndex { + let focusedItemId = currentSlice.items[nextIndex].id + self.focusedItemId = focusedItemId + self.state?.updated(transition: .immediate) + + self.currentSliceDisposable?.dispose() + self.currentSliceDisposable = (currentSlice.update( + currentSlice, + focusedItemId + ) + |> deliverOnMainQueue).start(next: { [weak self] contentSlice in + guard let self else { + return + } + self.currentSlice = contentSlice + self.state?.updated(transition: .immediate) + }) + } else { + self.environment?.controller()?.dismiss() + } + } + } + } + ) + }, containerSize: itemLayout.size ) if let view = visibleItem.view.view { @@ -377,6 +457,1444 @@ private final class StoryContainerScreenComponent: Component { } } + private func clearInputText() { + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + inputPanelView.clearSendMessageInput() + } + + private enum AttachMenuSubject { + case `default` + } + + private func presentAttachmentMenu(subject: AttachMenuSubject) { + guard let component = self.component else { + return + } + guard let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) else { + return + } + guard let targetMessageId = focusedItem.targetMessageId else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + + var inputText = NSAttributedString(string: "") + switch inputPanelView.getSendMessageInput() { + case let .text(text): + inputText = NSAttributedString(string: text) + } + + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: targetMessageId) + ) + |> deliverOnMainQueue).start(next: { [weak self] targetMessage in + guard let self, let component = self.component else { + return + } + guard let targetMessage, let peer = targetMessage.author else { + return + } + + let inputIsActive = !"".isEmpty + + self.endEditing(true) + + var banSendText: (Int32, Bool)? + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + var bannedSendFiles: (Int32, Bool)? + + let _ = bannedSendFiles + + var canSendPolls = true + if case let .user(peer) = peer, peer.botInfo == nil { + canSendPolls = false + } else if case .secretChat = peer { + canSendPolls = false + } else if case let .channel(channel) = peer { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendFiles) { + bannedSendFiles = value + } + if let value = channel.hasBannedPermission(.banSendText) { + banSendText = value + } + if channel.hasBannedPermission(.banSendPolls) != nil { + canSendPolls = false + } + } else if case let .legacyGroup(group) = peer { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendFiles) { + bannedSendFiles = (Int32.max, false) + } + if group.hasBannedPermission(.banSendText) { + banSendText = (Int32.max, false) + } + if group.hasBannedPermission(.banSendPolls) { + canSendPolls = false + } + } + + var availableButtons: [AttachmentButtonType] = [.gallery, .file] + if banSendText == nil { + availableButtons.append(.location) + availableButtons.append(.contact) + } + if canSendPolls { + availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) + } + + let isScheduledMessages = !"".isEmpty + + var peerType: AttachMenuBots.Bot.PeerFlags = [] + if case let .user(user) = peer { + if let _ = user.botInfo { + peerType.insert(.bot) + } else { + peerType.insert(.user) + } + } else if case .legacyGroup = peer { + peerType = .group + } else if case let .channel(channel) = peer { + if case .broadcast = channel.info { + peerType = .channel + } else { + peerType = .group + } + } + + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> + if !isScheduledMessages { + buttons = component.context.engine.messages.attachMenuBots() + |> map { attachMenuBots in + var buttons = availableButtons + var allButtons = availableButtons + var initialButton: AttachmentButtonType? + switch subject { + case .default: + initialButton = .gallery + /*case .edit: + break + case .gift: + initialButton = .gift*/ + } + + for bot in attachMenuBots.reversed() { + var peerType = peerType + if bot.peer.id == peer.id { + peerType.insert(.sameBot) + peerType.remove(.bot) + } + let button: AttachmentButtonType = .app(bot.peer, bot.shortName, bot.icons) + if !bot.peerTypes.intersection(peerType).isEmpty { + buttons.insert(button, at: 1) + + /*if case let .bot(botId, _, _) = subject { + if initialButton == nil && bot.peer.id == botId { + initialButton = button + } + }*/ + } + allButtons.insert(button, at: 1) + } + + return (buttons, allButtons, initialButton) + } + } else { + buttons = .single((availableButtons, availableButtons, .gallery)) + } + + let dataSettings = component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + let premiumGiftOptions: [CachedPremiumGiftOption] + if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, case let .user(user) = peer, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + premiumGiftOptions = []//self.presentationInterfaceState.premiumGiftOptions + //TODO:premium gift options + } else { + premiumGiftOptions = [] + } + + let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).start(next: { [weak self] buttonsAndInitialButton, dataSettings in + guard let self, let component = self.component else { + return + } + + var (buttons, allButtons, initialButton) = buttonsAndInitialButton + if !premiumGiftOptions.isEmpty { + buttons.insert(.gift, at: 1) + } + let _ = allButtons + + guard let initialButton = initialButton else { + /*if case let .bot(botId, botPayload, botJustInstalled) = subject { + if let button = allButtons.first(where: { button in + if case let .app(botPeer, _, _) = button, botPeer.id == botId { + return true + } else { + return false + } + }), case let .app(_, botName, _) = button { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller().present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } else { + let _ = (context.engine.messages.getAttachMenuBot(botId: botId) + |> deliverOnMainQueue).start(next: { [weak self] bot in + guard let self, let component = self.component else { + return + } + + let peer = EnginePeer(bot.peer) + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) + |> deliverOnMainQueue).start(error: { _ in + }, completed: { + //TODO:present attachment bot + //strongSelf.presentAttachmentBot(botId: botId, payload: botPayload, justInstalled: true) + }) + }) + self.environment?.controller().present(controller, in: .window(.root)) + }, error: { [weak self] _ in + guard let self, let component = self.component else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller().present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + }*/ + return + } + + let currentMediaController = Atomic(value: nil) + let currentFilesController = Atomic(value: nil) + let currentLocationController = Atomic(value: nil) + + let attachmentController = AttachmentController( + context: component.context, + updatedPresentationData: nil, + chatLocation: .peer(id: peer.id), + buttons: buttons, + initialButton: initialButton, + makeEntityInputView: { [weak self] in + guard let self, let component = self.component else { + return nil + } + return EntityInputView( + context: component.context, + isDark: true, + areCustomEmojiEnabled: true //TODO:check custom emoji + ) + } + ) + attachmentController.didDismiss = { [weak self] in + guard let self else { + return + } + self.attachmentController = nil + } + attachmentController.getSourceRect = { [weak self] in + guard let self else { + return nil + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return nil + } + guard let attachmentButtonView = inputPanelView.getAttachmentButtonView() else { + return nil + } + return attachmentButtonView.convert(attachmentButtonView.bounds, to: self) + } + attachmentController.requestController = { [weak self, weak attachmentController] type, completion in + guard let self else { + return + } + switch type { + case .gallery: + self.controllerNavigationDisposable.set(nil) + let existingController = currentMediaController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + self.presentMediaPicker( + peer: peer, + replyToMessageId: targetMessageId, + saveEditedPhotos: dataSettings.storeEditedPhotos, + bannedSendPhotos: bannedSendPhotos, + bannedSendVideos: bannedSendVideos, + present: { controller, mediaPickerContext in + let _ = currentMediaController.swap(controller) + if !inputText.string.isEmpty { + mediaPickerContext?.setCaption(inputText) + } + completion(controller, mediaPickerContext) + }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in + attachmentController?.mediaPickerContext = mediaPickerContext + }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let self else { + return + } + if !inputText.string.isEmpty { + self.clearInputText() + } + self.enqueueMediaMessages(peer: peer, replyToMessageId: targetMessageId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + } + ) + case .file: + self.controllerNavigationDisposable.set(nil) + let existingController = currentFilesController.with { $0 } + if let controller = existingController as? AttachmentContainable, let mediaPickerContext = controller.mediaPickerContext { + completion(controller, mediaPickerContext) + controller.prepareForReuse() + return + } + let controller = component.context.sharedContext.makeAttachmentFileController(context: component.context, updatedPresentationData: nil, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in + guard let self else { + return + } + attachmentController?.dismiss(animated: true) + self.presentFileGallery(peer: peer, replyMessageId: targetMessageId) + }, presentFiles: { [weak self, weak attachmentController] in + guard let self else { + return + } + attachmentController?.dismiss(animated: true) + self.presentICloudFileGallery(peer: peer, replyMessageId: targetMessageId) + }, send: { [weak self] mediaReference in + guard let self, let component = self.component else { + return + } + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(targetMessageId)]) + |> deliverOnMainQueue).start() + + if let controller = self.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .succeed(text: "Message Sent"), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } + }) + let _ = currentFilesController.swap(controller) + if let controller = controller as? AttachmentContainable, let mediaPickerContext = controller.mediaPickerContext { + completion(controller, mediaPickerContext) + } + case .location: + self.controllerNavigationDisposable.set(nil) + let existingController = currentLocationController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + let selfPeerId: EnginePeer.Id + if case let .channel(peer) = peer, case .broadcast = peer.info { + selfPeerId = peer.id + } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { + selfPeerId = peer.id + } else { + selfPeerId = component.context.account.peerId + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] selfPeer in + guard let self, let component = self.component, let selfPeer else { + return + } + let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != component.context.account.peerId + let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in + guard let self else { + return + } + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: targetMessageId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + self.sendMessages(peer: peer, messages: [message]) + }) + completion(controller, controller.mediaPickerContext) + + let _ = currentLocationController.swap(controller) + }) + case .contact: + let contactsController = component.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: component.context, updatedPresentationData: nil, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) + contactsController.presentScheduleTimePicker = { [weak self] completion in + guard let self else { + return + } + self.presentScheduleTimePicker(peer: peer, completion: completion) + } + contactsController.navigationPresentation = .modal + if let contactsController = contactsController as? AttachmentContainable, let mediaPickerContext = contactsController.mediaPickerContext { + completion(contactsController, mediaPickerContext) + } + self.controllerNavigationDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self, let (peers, _, silent, scheduleTime, text) = peers else { + return + } + + let targetPeer = peer + + var textEnqueueMessage: EnqueueMessage? + if let text = text, text.length > 0 { + var attributes: [EngineMessage.Attribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + } + if peers.count > 1 { + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + for peer in peers { + var media: TelegramMediaContact? + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue + } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) + } + + if let media = media { + let replyMessageId = targetMessageId + /*strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil)*/ + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + enqueueMessages.append(message) + } + } + + self.sendMessages(peer: peer, messages: self.transformEnqueueMessages(messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else if let peer = peers.first { + let dataSignal: Signal<(EnginePeer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + let context = component.context + dataSignal = (component.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) + |> take(1) + |> mapToSignal { basicData -> Signal<(EnginePeer?, DeviceContactExtendedData?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + + if let stableId = stableId { + return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (EnginePeer?, DeviceContactExtendedData?) in + return (EnginePeer(contact), extendedData) + } + } else { + return .single((EnginePeer(contact), contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = (component.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (EnginePeer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + self.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).start(next: { [weak self] peerAndContactData in + guard let self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 else { + return + } + if contactData.isPrimitive { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageId = targetMessageId + /*strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil)*/ + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + + self.sendMessages(peer: targetPeer, messages: self.transformEnqueueMessages(messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else { + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: component.context, subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self] peer, contactData in + guard let self else { + return + } + if contactData.basicData.phoneNumbers.isEmpty { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageId = targetMessageId + /*strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil)*/ + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + + self.sendMessages(peer: targetPeer, messages: self.transformEnqueueMessages(messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } + }), completed: nil, cancelled: nil) + self.environment?.controller()?.push(contactController) + } + })) + } + })) + case .poll: + let controller = self.configurePollCreation(peer: peer, targetMessageId: targetMessageId) + completion(controller, controller?.mediaPickerContext) + self.controllerNavigationDisposable.set(nil) + case .gift: + /*let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions + if !premiumGiftOptions.isEmpty { + let controller = PremiumGiftScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + if let strongSelf = self { + strongSelf.push(c) + } + }, completion: { [weak self] in + if let strongSelf = self { + strongSelf.hintPlayNextOutgoingGift() + strongSelf.attachmentController?.dismiss(animated: true) + } + }) + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() + }*/ + //TODO:gift controller + break + case let .app(bot, botName, _): + var payload: String? + var fromAttachMenu = true + /*if case let .bot(_, botPayload, _) = subject { + payload = botPayload + fromAttachMenu = false + }*/ + payload = nil + fromAttachMenu = true + let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false) + let replyMessageId = targetMessageId + let controller = WebAppController(context: component.context, updatedPresentationData: nil, params: params, replyToMessageId: replyMessageId, threadId: nil) + controller.openUrl = { [weak self] url in + guard let self else { + return + } + let _ = self + //self?.openUrl(url, concealed: true, forceExternal: true) + } + controller.getNavigationController = { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return nil + } + return controller.navigationController as? NavigationController + } + controller.completion = { [weak self] in + guard let self else { + return + } + let _ = self + /*if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + }*/ + } + completion(controller, controller.mediaPickerContext) + self.controllerNavigationDisposable.set(nil) + default: + break + } + } + let present = { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + attachmentController.navigationPresentation = .flatModal + controller.push(attachmentController) + self.attachmentController = attachmentController + } + + if inputIsActive { + Queue.mainQueue().after(0.15, { + present() + }) + } else { + present() + } + }) + }) + } + + private func presentMediaPicker( + peer: EnginePeer, + replyToMessageId: EngineMessage.Id?, + subject: MediaPickerScreen.Subject = .assets(nil, .default), + saveEditedPhotos: Bool, + bannedSendPhotos: (Int32, Bool)?, + bannedSendVideos: (Int32, Bool)?, + present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, + updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, + completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void + ) { + guard let component = self.component else { + return + } + let controller = MediaPickerScreen(context: component.context, updatedPresentationData: nil, peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + let mediaPickerContext = controller.mediaPickerContext + controller.openCamera = { [weak self] cameraView in + guard let self else { + return + } + self.openCamera(peer: peer, replyToMessageId: replyToMessageId, cameraView: cameraView) + } + controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in + guard let self, let controller else { + return + } + self.presentWebSearch(editingMessage: false, attachment: true, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in + controller?.present(c, in: .current) + if let webSearchController = c as? WebSearchController { + webSearchController.searchingUpdated = { [weak mediaGroups] searching in + if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0) + mediaGroups.displayNode.isUserInteractionEnabled = !searching + } + } + webSearchController.present(mediaGroups, in: .current) + webSearchController.dismissed = { + updateMediaPickerContext(mediaPickerContext) + } + controller?.webSearchController = webSearchController + updateMediaPickerContext(webSearchController.mediaPickerContext) + } + }) + } + controller.presentSchedulePicker = { [weak self] media, done in + guard let self else { + return + } + self.presentScheduleTimePicker(peer: peer, style: media ? .media : .default, completion: { time in + done(time) + }) + } + controller.presentTimerPicker = { [weak self] done in + guard let self else { + return + } + self.presentTimerPicker(peer: peer, style: .media, completion: { time in + done(time) + }) + } + controller.getCaptionPanelView = { [weak self] in + guard let self else { + return nil + } + return self.getCaptionPanelView(peer: peer) + } + controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in + completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) + } + present(controller, mediaPickerContext) + } + + private func presentOldMediaPicker(peer: EnginePeer, replyMessageId: EngineMessage.Id?, fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { + guard let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + var inputText = NSAttributedString(string: "") + switch inputPanelView.getSendMessageInput() { + case let .text(text): + inputText = NSAttributedString(string: text) + } + + let engine = component.context.engine + let _ = (component.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + + return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) + |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in + return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) + } + } + |> switchToLatest + |> deliverOnMainQueue).start(next: { [weak self] settings, searchBotsConfiguration in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + var selectionLimit: Int = 100 + var slowModeEnabled = false + if case let .channel(channel) = peer, channel.isRestrictedBySlowmode { + selectionLimit = 10 + slowModeEnabled = true + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let _ = legacyAssetPicker(context: component.context, presentationData: presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer._asPeer(), threadTitle: nil, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).start(next: { generator in + if let strongSelf = self, let component = strongSelf.component, let controller = strongSelf.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: presentationData.theme, initialLayout: controller.currentlyAppliedLayout) + legacyController.navigationPresentation = .modal + legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + legacyController?.view.disablesInteractiveModalDismiss = true + } + let controller = generator(legacyController.context) + + legacyController.bind(controller: controller) + legacyController.deferScreenEdgeGestures = [.top] + + configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak legacyController] in + if let strongSelf = self, let component = strongSelf.component { + let controller = WebSearchController(context: component.context, updatedPresentationData: nil, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in + if let legacyController = legacyController { + legacyController.dismiss() + } + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(peer: peer, replyMessageId: replyMessageId, results: results, result: result, hideVia: true) + } + }, enqueueMediaMessages: { signals in + if let strongSelf = self { + if editingMedia { + strongSelf.editMessageMediaWithLegacySignals(signals) + } else { + strongSelf.enqueueMediaMessages(peer: peer, replyToMessageId: replyMessageId, signals: signals, silentPosting: silentPosting) + } + } + }) + })) + controller.getCaptionPanelView = { + guard let self else { + return nil + } + return self.getCaptionPanelView(peer: peer) + } + strongSelf.environment?.controller()?.push(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + + let text: String + if slowModeEnabled { + text = presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = presentationData.strings.Chat_AttachmentLimitReached + } + + strongSelf.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentSchedulePicker: { media, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(peer: peer, style: media ? .media : .default, completion: { time in + done(time) + }) + } + }, presentTimerPicker: { done in + if let strongSelf = self { + strongSelf.presentTimerPicker(peer: peer, style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { + guard let self else { + return nil + } + return self.getCaptionPanelView(peer: peer) + }) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak legacyController] signals, silentPosting, scheduleTime in + if let legacyController = legacyController { + legacyController.dismiss(animated: true) + completion(signals!, silentPosting, scheduleTime) + } + } + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss(animated: true) + } + } + strongSelf.endEditing(true) + present(legacyController, LegacyAssetPickerContext(controller: controller)) + } + }) + }) + } + + private func presentFileGallery(peer: EnginePeer, replyMessageId: EngineMessage.Id?, editingMessage: Bool = false) { + self.presentOldMediaPicker(peer: peer, replyMessageId: replyMessageId, fileMode: true, editingMedia: editingMessage, present: { [weak self] c, _ in + self?.environment?.controller()?.push(c) + }, completion: { [weak self] signals, silentPosting, scheduleTime in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals) + } else { + self?.enqueueMediaMessages(peer: peer, replyToMessageId: replyMessageId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + } + }) + } + + private func presentICloudFileGallery(peer: EnginePeer, replyMessageId: EngineMessage.Id?) { + guard let component = self.component else { + return + } + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + strongSelf.environment?.controller()?.present(legacyICloudFilePicker(theme: presentationData.theme, completion: { [weak self] urls in + if let strongSelf = self, !urls.isEmpty { + var signals: [Signal] = [] + for url in urls { + signals.append(iCloudFileDescription(url)) + } + strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) + |> deliverOnMainQueue).start(next: { results in + if let strongSelf = self, let component = strongSelf.component { + for item in results { + if let item = item { + if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { + let controller = PremiumLimitScreen(context: component.context, subject: .files, count: 4, action: { + }) + strongSelf.environment?.controller()?.push(controller) + return + } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { + let context = component.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: { + replaceImpl?(PremiumIntroScreen(context: context, source: .upload)) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.environment?.controller()?.push(controller) + return + } + } + } + + var groupingKey: Int64? + var fileTypes: (music: Bool, other: Bool) = (false, false) + if results.count > 1 { + for item in results { + if let item = item { + let pathExtension = (item.fileName as NSString).pathExtension.lowercased() + if ["mp3", "m4a"].contains(pathExtension) { + fileTypes.music = true + } else { + fileTypes.other = true + } + } + } + } + if fileTypes.music != fileTypes.other { + groupingKey = Int64.random(in: Int64.min ... Int64.max) + } + + var messages: [EnqueueMessage] = [] + for item in results { + if let item = item { + let fileId = Int64.random(in: Int64.min ... Int64.max) + let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + } + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(.FileName(fileName: item.fileName)) + if let audioMetadata = item.audioMetadata { + attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) + } + + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + messages.append(message) + } + if let _ = groupingKey, messages.count % 10 == 0 { + groupingKey = Int64.random(in: Int64.min ... Int64.max) + } + } + + if !messages.isEmpty { + strongSelf.sendMessages(peer: peer, messages: messages) + } + } + })) + } + }), in: .window(.root)) + }) + } + + private func enqueueChatContextResult(peer: EnginePeer, replyMessageId: EngineMessage.Id?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { + if !canSendMessagesToPeer(peer._asPeer()) { + return + } + + let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in + guard let self, let component = self.component else { + return + } + if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + } + } + + sendMessage(nil) + } + + private func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { + /*guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) + |> deliverOnMainQueue).start(next: { [weak self] configuration in + if let strongSelf = self { + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in + self?.attachmentController?.dismiss(animated: true, completion: nil) + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(results, result, hideVia: true) + } + }, enqueueMediaMessages: { [weak self] signals in + if let strongSelf = self, !signals.isEmpty { + if editingMessage { + strongSelf.editMessageMediaWithLegacySignals(signals) + } else { + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + } + } + }) + }), activateOnDisplay: activateOnDisplay) + controller.attemptItemSelection = { [weak strongSelf] item in + guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return false + } + + enum ItemType { + case gif + case image + case video + } + + var itemType: ItemType? + switch item { + case let .internalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + case let .externalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + var bannedSendGifs: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendGifs) { + bannedSendGifs = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendGifs) { + bannedSendGifs = (Int32.max, false) + } + } + + if let itemType { + switch itemType { + case .image: + if bannedSendPhotos != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .video: + if bannedSendVideos != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .gif: + if bannedSendGifs != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } + } + + return true + } + controller.getCaptionPanelView = { [weak strongSelf] in + return strongSelf?.getCaptionPanelView() + } + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + })*/ + } + + private func getCaptionPanelView(peer: EnginePeer) -> TGCaptionPanelView? { + guard let component = self.component else { + return nil + } + //TODO:self.presentationInterfaceState.customEmojiAvailable + return component.context.sharedContext.makeGalleryCaptionPanelView(context: component.context, chatLocation: .peer(id: peer.id), customEmojiAvailable: true, present: { [weak self] c in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root)) + }, presentInGlobalOverlay: { [weak self] c in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c) + }) as? TGCaptionPanelView + } + + private func openCamera(peer: EnginePeer, replyToMessageId: EngineMessage.Id?, cameraView: TGAttachmentCameraView? = nil) { + guard let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + + var inputText = NSAttributedString(string: "") + switch inputPanelView.getSendMessageInput() { + case let .text(text): + inputText = NSAttributedString(string: text) + } + + let _ = (component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let self, let component = self.component, let parentController = self.environment?.controller() else { + return + } + + var enablePhoto = true + var enableVideo = true + + if let callManager = component.context.sharedContext.callManager, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if case let .channel(channel) = peer { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if case let .legacyGroup(group) = peer { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat + + presentedLegacyCamera(context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), cameraView: cameraView, menuController: nil, parentController: parentController, attachmentController: self.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + guard let self else { + return + } + self.enqueueMediaMessages(peer: peer, replyToMessageId: replyToMessageId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + if !inputText.string.isEmpty { + self.clearInputText() + } + }, recognizedQRCode: { _ in + }, presentSchedulePicker: { [weak self] _, done in + guard let self else { + return + } + self.presentScheduleTimePicker(peer: peer, style: .media, completion: { time in + done(time) + }) + }, presentTimerPicker: { [weak self] done in + guard let self else { + return + } + self.presentTimerPicker(peer: peer, style: .media, completion: { time in + done(time) + }) + }, getCaptionPanelView: { [weak self] in + guard let self else { + return nil + } + return self.getCaptionPanelView(peer: peer) + }, dismissedWithResult: { [weak self] in + guard let self else { + return + } + self.attachmentController?.dismiss(animated: false, completion: nil) + }, finishedTransitionIn: { [weak self] in + guard let self else { + return + } + self.attachmentController?.scrollToTop?() + }) + }) + } + + private func presentScheduleTimePicker( + peer: EnginePeer, + style: ChatScheduleTimeControllerStyle = .default, + selectedTime: Int32? = nil, + dismissByTapOutside: Bool = true, + completion: @escaping (Int32) -> Void + ) { + guard let component = self.component else { + return + } + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Presence(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { [weak self] presence in + guard let self, let component = self.component else { + return + } + + var sendWhenOnlineAvailable = false + if let presence, case .present = presence.status { + sendWhenOnlineAvailable = true + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + let mode: ChatScheduleTimeControllerMode + if peer.id == component.context.account.peerId { + mode = .reminders + } else { + mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) + } + let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: nil, peerId: peer.id, mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in + completion(time) + }) + self.endEditing(true) + self.environment?.controller()?.present(controller, in: .window(.root)) + }) + } + + private func presentTimerPicker(peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + guard let component = self.component else { + return + } + let controller = ChatTimerScreen(context: component.context, updatedPresentationData: nil, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in + completion(time) + }) + self.endEditing(true) + self.environment?.controller()?.present(controller, in: .window(.root)) + } + + private func configurePollCreation(peer: EnginePeer, targetMessageId: EngineMessage.Id, isQuiz: Bool? = nil) -> CreatePollControllerImpl? { + guard let component = self.component else { + return nil + } + return createPollController(context: component.context, updatedPresentationData: nil, peer: peer, isQuiz: isQuiz, completion: { [weak self] poll in + guard let self else { + return + } + let replyMessageId = targetMessageId + /*strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil)*/ + let message: EnqueueMessage = .message( + text: "", + attributes: [], + inlineStickers: [:], + mediaReference: .standalone(media: TelegramMediaPoll( + pollId: EngineMedia.Id(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)), + publicity: poll.publicity, + kind: poll.kind, + text: poll.text, + options: poll.options, + correctAnswers: poll.correctAnswers, + results: poll.results, + isClosed: false, + deadlineTimeout: poll.deadlineTimeout + )), + replyToMessageId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ) + self.sendMessages(peer: peer, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]) + }) + } + + private func transformEnqueueMessages(messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { + guard let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) else { + return [] + } + guard let targetMessageId = focusedItem.targetMessageId else { + return [] + } + + let defaultReplyMessageId: EngineMessage.Id? = targetMessageId + + return messages.map { message in + var message = message + + if let defaultReplyMessageId = defaultReplyMessageId { + switch message { + case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + if replyToMessageId == nil { + message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + } + case .forward: + break + } + } + + return message.withUpdatedAttributes { attributes in + var attributes = attributes + if silentPosting || scheduleTime != nil { + for i in (0 ..< attributes.count).reversed() { + if attributes[i] is NotificationInfoMessageAttribute { + attributes.remove(at: i) + } else if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute { + attributes.remove(at: i) + } + } + if silentPosting { + attributes.append(NotificationInfoMessageAttribute(flags: .muted)) + } + if let scheduleTime = scheduleTime { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) + } + } + return attributes + } + } + } + + private func sendMessages(peer: EnginePeer, messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { + guard let component = self.component else { + return + } + let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: self.transformEnqueueMessages(messages: messages, silentPosting: false)) + |> deliverOnMainQueue).start() + + donateSendMessageIntent(account: component.context.account, sharedContext: component.context.sharedContext, intentContext: .chat, peerIds: [peer.id]) + + if let controller = self.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .succeed(text: "Message Sent"), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } + } + + private func enqueueMediaMessages(peer: EnginePeer, replyToMessageId: EngineMessage.Id?, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + guard let component = self.component else { + return + } + + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals!) + |> deliverOnMainQueue).start(next: { [weak self] items in + if let strongSelf = self { + var mappedMessages: [EnqueueMessage] = [] + var addedTransitions: [(Int64, [String], () -> Void)] = [] + + var groupedCorrelationIds: [Int64: Int64] = [:] + + var skipAddingTransitions = false + + for item in items { + var message = item.message + if message.groupingKey != nil { + if items.count > 10 { + skipAddingTransitions = true + } + } else if items.count > 3 { + skipAddingTransitions = true + } + + if let uniqueId = item.uniqueId, !item.isFile && !skipAddingTransitions { + let correlationId: Int64 + var addTransition = scheduleTime == nil + if let groupingKey = message.groupingKey { + if let existing = groupedCorrelationIds[groupingKey] { + correlationId = existing + addTransition = false + } else { + correlationId = Int64.random(in: 0 ..< Int64.max) + groupedCorrelationIds[groupingKey] = correlationId + } + } else { + correlationId = Int64.random(in: 0 ..< Int64.max) + } + message = message.withUpdatedCorrelationId(correlationId) + + if addTransition { + addedTransitions.append((correlationId, [uniqueId], addedTransitions.isEmpty ? completion : {})) + } else { + if let index = addedTransitions.firstIndex(where: { $0.0 == correlationId }) { + var (correlationId, uniqueIds, completion) = addedTransitions[index] + uniqueIds.append(uniqueId) + addedTransitions[index] = (correlationId, uniqueIds, completion) + } + } + } + mappedMessages.append(message) + } + + let messages = strongSelf.transformEnqueueMessages(messages: mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) + + strongSelf.sendMessages(peer: peer, messages: messages.map { $0.withUpdatedReplyToMessageId(replyToMessageId) }, media: true) + + if let _ = scheduleTime { + completion() + } + } + })) + } + + private func editMessageMediaWithLegacySignals(_ signals: [Any]) { + guard let component = self.component else { + return + } + let _ = (legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals) + |> deliverOnMainQueue).start() + } + private func updatePreloads() { var validIds: [AnyHashable] = [] if let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) { @@ -505,12 +2023,26 @@ private final class StoryContainerScreenComponent: Component { return } self.performSendMessageAction() + }, + attachmentAction: { [weak self] in + guard let self else { + return + } + self.presentAttachmentMenu(subject: .default) } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) ) + let footerPanelSize = self.footerPanel.update( + transition: transition, + component: AnyComponent(StoryFooterPanelComponent( + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 200.0) + ) + let bottomContentInsetWithoutInput = bottomContentInset let inputPanelBottomInset: CGFloat @@ -539,6 +2071,11 @@ private final class StoryContainerScreenComponent: Component { transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size)) } + var focusedItem: StoryContentItem? + if let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == self.focusedItemId }) { + focusedItem = item + } + var currentRightInfoItem: InfoItem? if let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == self.focusedItemId }) { if let rightInfoComponent = item.rightInfoComponent { @@ -629,7 +2166,7 @@ private final class StoryContainerScreenComponent: Component { } } - if let currentSlice = self.currentSlice { + if let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let visibleItem = self.visibleItems[focusedItemId] { let navigationStripSideInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0 @@ -641,7 +2178,11 @@ private final class StoryContainerScreenComponent: Component { index: max(0, min(currentSlice.totalCount - 1 - index, currentSlice.totalCount - 1)), count: currentSlice.totalCount )), - environment: {}, + environment: { + MediaNavigationStripComponent.EnvironmentType( + currentProgress: visibleItem.currentProgress + ) + }, containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0) ) if let navigationStripView = self.navigationStrip.view { @@ -652,19 +2193,22 @@ private final class StoryContainerScreenComponent: Component { } if let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) { + var items: [StoryActionsComponent.Item] = [] + if !focusedItem.isMy { + items.append(StoryActionsComponent.Item( + kind: .like, + isActivated: focusedItem.hasLike + )) + } + items.append(StoryActionsComponent.Item( + kind: .share, + isActivated: false + )) + let inlineActionsSize = self.inlineActions.update( transition: transition, component: AnyComponent(StoryActionsComponent( - items: [ - StoryActionsComponent.Item( - kind: .like, - isActivated: focusedItem.hasLike - ), - StoryActionsComponent.Item( - kind: .share, - isActivated: false - ) - ], + items: items, action: { [weak self] item in guard let self else { return @@ -697,7 +2241,18 @@ private final class StoryContainerScreenComponent: Component { self.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + transition.setAlpha(view: inputPanelView, alpha: focusedItem?.isMy == true ? 0.0 : 1.0) } + + let footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) + if let footerPanelView = self.footerPanel.view { + if footerPanelView.superview == nil { + self.addSubview(footerPanelView) + } + transition.setFrame(view: footerPanelView, frame: footerPanelFrame) + transition.setAlpha(view: footerPanelView, alpha: focusedItem?.isMy == true ? 1.0 : 0.0) + } + let bottomGradientHeight = inputPanelSize.height + 32.0 transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - environment.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight))) transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 194a52d30b..90670cdacc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -6,24 +6,51 @@ import SwiftSignalKit import TelegramCore public final class StoryContentItem { + public final class ExternalState { + public init() { + } + } + + public final class Environment: Equatable { + public let externalState: ExternalState + public let presentationProgressUpdated: (Double) -> Void + + public init( + externalState: ExternalState, + presentationProgressUpdated: @escaping (Double) -> Void + ) { + self.externalState = externalState + self.presentationProgressUpdated = presentationProgressUpdated + } + + public static func ==(lhs: Environment, rhs: Environment) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + return true + } + } + public let id: AnyHashable public let position: Int - public let component: AnyComponent + public let component: AnyComponent public let centerInfoComponent: AnyComponent? public let rightInfoComponent: AnyComponent? public let targetMessageId: EngineMessage.Id? public let preload: Signal? public let hasLike: Bool + public let isMy: Bool public init( id: AnyHashable, position: Int, - component: AnyComponent, + component: AnyComponent, centerInfoComponent: AnyComponent?, rightInfoComponent: AnyComponent?, targetMessageId: EngineMessage.Id?, preload: Signal?, - hasLike: Bool + hasLike: Bool, + isMy: Bool ) { self.id = id self.position = position @@ -33,6 +60,7 @@ public final class StoryContentItem { self.targetMessageId = targetMessageId self.preload = preload self.hasLike = hasLike + self.isMy = isMy } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 59f6050388..741761fd8f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -65,7 +65,8 @@ public enum StoryChatContent { }, targetMessageId: entry.message.id, preload: preload, - hasLike: hasLike + hasLike: hasLike, + isMy: false//!entry.message.effectivelyIncoming(context.account.peerId) )) } return StoryContentItemSlice( diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift index 6c7ba42183..8538cce439 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift @@ -9,8 +9,11 @@ import PhotoResources import SwiftSignalKit import UniversalMediaPlayer import TelegramUniversalVideoContent +import StoryContainerScreen final class StoryMessageContentComponent: Component { + typealias EnvironmentType = StoryContentItem.Environment + let context: AccountContext let message: EngineMessage @@ -88,6 +91,11 @@ final class StoryMessageContentComponent: Component { private var component: StoryMessageContentComponent? private weak var state: EmptyComponentState? + private var environment: StoryContentItem.Environment? + + private var currentProgressStart: Double? + private var currentProgressTimer: SwiftSignalKit.Timer? + private var videoProgressDisposable: Disposable? override init(frame: CGRect) { self.imageNode = TransformImageNode() @@ -103,6 +111,8 @@ final class StoryMessageContentComponent: Component { deinit { self.fetchDisposable?.dispose() + self.currentProgressTimer?.invalidate() + self.videoProgressDisposable?.dispose() } private func performActionAfterImageContentLoaded(update: Bool) { @@ -149,9 +159,10 @@ final class StoryMessageContentComponent: Component { } } - func update(component: StoryMessageContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryMessageContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state + self.environment = environment[StoryContentItem.Environment.self].value var messageMedia: EngineMedia? for media in component.message.media { @@ -266,6 +277,36 @@ final class StoryMessageContentComponent: Component { self.imageNode.frame = CGRect(origin: CGPoint(), size: availableSize) } + if let videoNode = self.videoNode { + if self.videoProgressDisposable == nil { + self.videoProgressDisposable = (videoNode.status + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self, let status, status.duration > 0.0 else { + return + } + let currentProgress = Double(status.timestamp / status.duration) + let clippedProgress = max(0.0, min(1.0, currentProgress)) + self.environment?.presentationProgressUpdated(clippedProgress) + }) + } + } else { + if self.currentProgressTimer == nil { + self.currentProgressStart = CFAbsoluteTimeGetCurrent() + self.currentProgressTimer = SwiftSignalKit.Timer( + timeout: 1.0 / 60.0, + repeat: true, + completion: { [weak self] in + guard let self, let currentProgressStart = self.currentProgressStart else { + return + } + let currentProgress = (CFAbsoluteTimeGetCurrent() - currentProgressStart) / 5.0 + let clippedProgress = max(0.0, min(1.0, currentProgress)) + self.environment?.presentationProgressUpdated(clippedProgress) + }, queue: .mainQueue()) + self.currentProgressTimer?.start() + } + } + return availableSize } } @@ -274,7 +315,7 @@ final class StoryMessageContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD new file mode 100644 index 0000000000..a1e64e32ef --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StoryFooterPanelComponent", + module_name = "StoryFooterPanelComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift new file mode 100644 index 0000000000..b1ff6892ed --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -0,0 +1,48 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import BundleIconComponent + +public final class StoryFooterPanelComponent: Component { + public init( + ) { + } + + public static func ==(lhs: StoryFooterPanelComponent, rhs: StoryFooterPanelComponent) -> Bool { + return true + } + + public final class View: UIView { + private let viewStatsText = ComponentView() + private let deleteButton = ComponentView() + private let moreButton = ComponentView() + + private var component: StoryFooterPanelComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let baseHeight: CGFloat = 44.0 + let size = CGSize(width: availableSize.width, height: baseHeight) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index a1e0ef24b3..f92270b25e 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -193,7 +193,7 @@ private final class AttachmentFileContext: AttachmentMediaPickerContext { } } -class AttachmentFileControllerImpl: ItemListController, AttachmentContainable { +class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } @@ -224,7 +224,7 @@ private struct AttachmentFileControllerState: Equatable { var searching: Bool } -func attachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileControllerImpl { +func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { let actionsDisposable = DisposableSet() let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 870b95e863..54e82f5b4c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -90,6 +90,9 @@ import FeaturedStickersScreen import ChatEntityKeyboardInputNode import StorageUsageScreen import AvatarEditorScreen +import ChatScheduleTimeController +import ICloudResources +import LegacyCamera #if DEBUG import os.signpost @@ -12670,105 +12673,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func getCaptionPanelView() -> TGCaptionPanelView { - let presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) - - var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? - var ensureFocusedImpl: (() -> Void)? - - let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in - updateChatPresentationInterfaceStateImpl?({ - let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode) - return $0.updatedInterfaceState { interfaceState in - return interfaceState.withUpdatedEffectiveInputState(updatedState) - }.updatedInputMode({ _ in updatedMode }) - }) - }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in - updateChatPresentationInterfaceStateImpl?({ - let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) - return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ - $0.withUpdatedMessageActionsState({ value in - var value = value - value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId - return value - }) - }) - }) - }, openLinkEditing: { [weak self] in - if let strongSelf = self { - var selectionRange: Range? - var text: NSAttributedString? - var inputMode: ChatInputMode? - updateChatPresentationInterfaceStateImpl?({ state in - selectionRange = state.interfaceState.effectiveInputState.selectionRange - if let selectionRange = selectionRange { - text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) - } - inputMode = state.inputMode - return state - }) - - var link: String? - if let text { - text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in - if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { - link = linkAttribute.url - } - } - } - - let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { link in - if let inputMode = inputMode, let selectionRange = selectionRange { - if let link = link { - updateChatPresentationInterfaceStateImpl?({ - return $0.updatedInterfaceState({ - $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) - }) - }) - } - ensureFocusedImpl?() - updateChatPresentationInterfaceStateImpl?({ - return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ - $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) - }) - }) - } - }) - strongSelf.present(controller, in: .window(.root)) + private func getCaptionPanelView() -> TGCaptionPanelView? { + return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in + self?.present(c, in: .window(.root)) + }, presentInGlobalOverlay: { [weak self] c in + guard let self else { + return } - }) - - let inputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { [weak self] c in - self?.presentInGlobalOverlay(c) - }, makeEntityInputView: { [weak self] in - guard let strongSelf = self else { - return nil - } - - return EntityInputView(context: strongSelf.context, isDark: true, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) - }) - inputPanelNode.interfaceInteraction = interfaceInteraction - inputPanelNode.effectivePresentationInterfaceState = { - return presentationInterfaceState - } - - updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in - let updatedPresentationInterfaceState = f(presentationInterfaceState) - let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState - - presentationInterfaceState = updatedPresentationInterfaceState - - if let inputPanelNode = inputPanelNode, updateInputTextState { - inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true) - } - } - - ensureFocusedImpl = { [weak inputPanelNode] in - inputPanelNode?.ensureFocused() - } - - return inputPanelNode + self.presentInGlobalOverlay(c) + }) as? TGCaptionPanelView } private func openCamera(cameraView: TGAttachmentCameraView? = nil) { @@ -13234,7 +13147,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G controller.prepareForReuse() return } - let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in + let controller = strongSelf.context.sharedContext.makeAttachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in attachmentController?.dismiss(animated: true) self?.presentFileGallery() }, presentFiles: { [weak self, weak attachmentController] in @@ -13252,8 +13165,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) }) - let _ = currentFilesController.swap(controller) - completion(controller, controller.mediaPickerContext) + if let controller = controller as? AttachmentFileControllerImpl { + let _ = currentFilesController.swap(controller) + completion(controller, controller.mediaPickerContext) + } case .location: strongSelf.controllerNavigationDisposable.set(nil) let existingController = currentLocationController.with { $0 } diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 893009e8bc..0a5dbe166a 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -343,7 +343,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.deactivateSearch() } - public var mediaPickerContext: AttachmentMediaPickerContext { + public var mediaPickerContext: AttachmentMediaPickerContext? { return ContactsPickerContext(controller: self) } diff --git a/submodules/TelegramUI/Sources/DeclareEncodables.swift b/submodules/TelegramUI/Sources/DeclareEncodables.swift index e9ec2a4b80..2d6d4c853b 100644 --- a/submodules/TelegramUI/Sources/DeclareEncodables.swift +++ b/submodules/TelegramUI/Sources/DeclareEncodables.swift @@ -12,6 +12,7 @@ import WallpaperResources import MediaResources import LocationUI import ChatInterfaceState +import ICloudResources private var telegramUIDeclaredEncodables: Void = { declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) }) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index aebb08f135..6e5d0895da 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -31,6 +31,10 @@ import ChatControllerInteraction import ChatPresentationInterfaceState import StorageUsageScreen import DebugSettingsUI +import TextFormat +import ChatTextLinkEditUI +import AttachmentTextInputPanelNode +import ChatEntityKeyboardInputNode private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1610,6 +1614,107 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } + public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { + return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, send: send) + } + + public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + + var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + + var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? + var ensureFocusedImpl: (() -> Void)? + + let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in + updateChatPresentationInterfaceStateImpl?({ + let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode) + return $0.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEffectiveInputState(updatedState) + }.updatedInputMode({ _ in updatedMode }) + }) + }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in + updateChatPresentationInterfaceStateImpl?({ + let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) + return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ + $0.withUpdatedMessageActionsState({ value in + var value = value + value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId + return value + }) + }) + }) + }, openLinkEditing: { + var selectionRange: Range? + var text: NSAttributedString? + var inputMode: ChatInputMode? + updateChatPresentationInterfaceStateImpl?({ state in + selectionRange = state.interfaceState.effectiveInputState.selectionRange + if let selectionRange = selectionRange { + text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) + } + inputMode = state.inputMode + return state + }) + + var link: String? + if let text { + text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in + if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { + link = linkAttribute.url + } + } + } + + let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in + if let inputMode = inputMode, let selectionRange = selectionRange { + if let link = link { + updateChatPresentationInterfaceStateImpl?({ + return $0.updatedInterfaceState({ + $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) + }) + }) + } + ensureFocusedImpl?() + updateChatPresentationInterfaceStateImpl?({ + return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ + $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) + }) + }) + } + }) + present(controller) + }) + + let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in + presentInGlobalOverlay(c) + }, makeEntityInputView: { + return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) + }) + inputPanelNode.interfaceInteraction = interfaceInteraction + inputPanelNode.effectivePresentationInterfaceState = { + return presentationInterfaceState + } + + updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in + let updatedPresentationInterfaceState = f(presentationInterfaceState) + let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState + + presentationInterfaceState = updatedPresentationInterfaceState + + if let inputPanelNode = inputPanelNode, updateInputTextState { + inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true) + } + } + + ensureFocusedImpl = { [weak inputPanelNode] in + inputPanelNode?.ensureFocused() + } + + return inputPanelNode + } + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController { let mappedSource: PremiumSource switch source { diff --git a/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift b/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift index 6c8e75a175..2009206428 100644 --- a/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift +++ b/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift @@ -11,6 +11,7 @@ import ChatInterfaceState import WallpaperResources import AppBundle import SwiftSignalKit +import ICloudResources func makeTelegramAccountAuxiliaryMethods(appDelegate: AppDelegate?) -> AccountAuxiliaryMethods { return AccountAuxiliaryMethods(fetchResource: { account, resource, ranges, _ in diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 45a1f1e678..847f69a9f3 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -17,6 +17,7 @@ import DebugSettingsUI import TabBarUI import WallpaperBackgroundNode import ChatPresentationInterfaceState +import LegacyCamera private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData