diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e186981260..2d3d1734c2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1180,7 +1180,7 @@ public protocol SharedAccountContext: AnyObject { func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) func openChatMessage(_ params: OpenChatMessageParams) -> Bool func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> - func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController + func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?, updateMusicSaved: ((FileMediaReference, Bool) -> Void)?, reorderSavedMusic: ((FileMediaReference, FileMediaReference?) -> Void)?) -> ViewController & OverlayAudioPlayerController func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index da7e2d67f2..fbba79185a 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1179,7 +1179,7 @@ public enum ChatHistoryListSource { } case `default` - case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, updateAll: Bool, canReorder: Bool, loadMore: (() -> Void)?) case customView(historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError>) } diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 8f12b31b2b..dba8c26977 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -26,7 +26,7 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId) case singleMessage(MessageId) case recentActions(Message) - case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, canReorder: Bool, at: MessageId, loadMore: (() -> Void)?) public var playlistId: PeerMessagesMediaPlaylistId { switch self { @@ -50,7 +50,7 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation public var messageId: MessageId? { switch self { - case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, messageId, _): + case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _): return messageId default: return nil @@ -85,8 +85,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation } else { return false } - case let .custom(_, lhsAt, _): - if case let .custom(_, rhsAt, _) = rhs, lhsAt == rhsAt { + case let .custom(_, _, lhsAt, _): + if case let .custom(_, _, rhsAt, _) = rhs, lhsAt == rhsAt { return true } else { return false diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index d0d6924d2c..e566c6b298 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -192,6 +192,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return } strongSelf.account = updatedAccount + strongSelf.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(strongSelf.engine)) } controller.loginWithNumber = { [weak self, weak controller] number, syncContacts in guard let self else { @@ -217,6 +218,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case let .sentCode(account): controller?.inProgress = false strongSelf.account = account + strongSelf.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(strongSelf.engine)) case .loggedIn: break } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index 963e68e5ab..8310c8f980 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -22,6 +22,7 @@ import PremiumCoinComponent import Markdown import CountrySelectionUI import AccountContext +import AlertUI final class AuthorizationSequencePaymentScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -142,12 +143,9 @@ final class AuthorizationSequencePaymentScreenComponent: Component { } if let errorText { - //addAppLogEvent(postbox: component.engine.account.postbox, type: "premium_gift.promo_screen_fail") - - let _ = errorText - let _ = controller - //let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - //controller.present(alertController, in: .window(.root)) + let theme = AlertControllerTheme(presentationData: presentationData) + let alertController = textAlertController(alertContext: AlertControllerContext(theme: theme, themeSignal: .single(theme)), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) } })) } else { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index a19bd6c521..5ad9daebf4 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3529,7 +3529,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else { playlistLocation = .custom(messages: foundMessages |> map { message, a, b in return (message.map { $0._asMessage() }, a, b) - }, at: message.id, loadMore: { + }, canReorder: false, at: message.id, loadMore: { loadMore() }) } @@ -4934,7 +4934,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: navigationController) + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: navigationController, updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if case let .messages(chatLocation, _, _) = playlistLocation { @@ -4975,7 +4975,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: navigationController) + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: navigationController, updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if index.1 { diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index d72686925c..b1e68a51c4 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -384,6 +384,8 @@ public final class ListMessageFileItemNode: ListMessageNode { private var contentSizeValue: CGSize? private var currentLeftOffset: CGFloat = 0.0 + var reorderControlNode: ItemListEditableReorderControlNode? + private var currentIsRestricted = false private var cachedSearchResult: CachedChatListSearchResult? @@ -568,6 +570,13 @@ public final class ListMessageFileItemNode: ListMessageNode { } } + override public func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { + return true + } + return false + } + override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = self.titleNode.asyncLayout() let textNodeMakeLayout = TextNode.asyncLayout(self.textNode) @@ -575,6 +584,7 @@ public final class ListMessageFileItemNode: ListMessageNode { let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode) let iconImageLayout = self.iconImageNode.asyncLayout() + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let currentMedia = self.currentMedia let currentMessage = self.message @@ -618,6 +628,7 @@ public final class ListMessageFileItemNode: ListMessageNode { var updatedStatusSignal: Signal? var updatedPlaybackStatusSignal: Signal? var updatedFetchControls: FetchControls? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? var isAudio = false var isVoice = false @@ -896,7 +907,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if statusUpdated && item.displayFileInfo { if let file = selectedMedia as? TelegramMediaFile { - updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) + updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || message.id.namespace == Namespaces.Message.Local, isDownloadList: item.isDownloadList) |> mapToSignal { value -> Signal in if case .Fetching = value.fetchStatus, !item.isDownloadList { return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) @@ -978,17 +989,25 @@ public final class ListMessageFileItemNode: ListMessageNode { captionText = text } + var reorderInset: CGFloat = 0.0 + + if item.canReorder { + let sizeAndApply = reorderControlLayout(item.presentationData.theme.theme) + reorderControlSizeAndApply = sizeAndApply + reorderInset = sizeAndApply.0 + } + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(item.context, params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, item.presentationData.theme.theme, titleText, titleExtraData) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(item.context, params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0 - reorderInset, item.presentationData.theme.theme, titleText, titleExtraData) - let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0 - reorderInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(item.context, params.width - leftInset - rightInset - 30.0, item.presentationData.theme.theme, descriptionText, descriptionExtraData) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(item.context, params.width - leftInset - rightInset - 30.0 - reorderInset, item.presentationData.theme.theme, descriptionText, descriptionExtraData) var (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) if extensionTextLayout.truncated, let text = extensionText?.string { @@ -1094,7 +1113,7 @@ public final class ListMessageFileItemNode: ListMessageNode { strongSelf.currentLeftOffset = leftOffset if let _ = updatedTheme { - if item.displayBackground { + if item.displayBackground || item.canReorder { let backgroundNode: ASDisplayNode if let current = strongSelf.backgroundNode { backgroundNode = current @@ -1103,7 +1122,7 @@ public final class ListMessageFileItemNode: ListMessageNode { strongSelf.backgroundNode = backgroundNode strongSelf.insertSubnode(backgroundNode, at: 0) } - backgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor + backgroundNode.backgroundColor = item.canReorder ? item.presentationData.theme.theme.list.plainBackgroundColor : item.presentationData.theme.theme.list.itemBlocksBackgroundColor } strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor @@ -1135,6 +1154,24 @@ public final class ListMessageFileItemNode: ListMessageNode { }) } + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: nodeLayout.contentSize.height)) + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1(nodeLayout.contentSize.height, false, .immediate) + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + reorderControlNode.frame = reorderControlFrame + } else if let reorderControlNode = strongSelf.reorderControlNode { + let _ = reorderControlSizeAndApply.1(nodeLayout.contentSize.height, false, .immediate) + transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) + } + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + } + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - UIScreenPixel), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel - nodeLayout.insets.bottom)) diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index f48e0ee89b..57e6d972ba 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -57,13 +57,14 @@ public final class ListMessageItem: ListViewItem { let isDownloadList: Bool let displayFileInfo: Bool let displayBackground: Bool + let canReorder: Bool let style: ItemListStyle let header: ListViewItemHeader? public let selectable: Bool = true - public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) { + public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) { self.presentationData = presentationData self.context = context self.chatLocation = chatLocation @@ -83,6 +84,7 @@ public final class ListMessageItem: ListViewItem { self.isDownloadList = isDownloadList self.displayFileInfo = displayFileInfo self.displayBackground = displayBackground + self.canReorder = canReorder self.style = style } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 7221744aee..e458dc3881 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -496,7 +496,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) } dict[1964968186] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGiveaway($0) } - dict[-572715178] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsTopup($0) } + dict[-106780981] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsTopup($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } dict[-1881255857] = { return Api.InputThemeSettings.parse_inputThemeSettings($0) } @@ -954,7 +954,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } - dict[12386139] = { return Api.StarGift.parse_starGift($0) } + dict[-2136190013] = { return Api.StarGift.parse_starGift($0) } dict[648369470] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } @@ -1430,8 +1430,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) } dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) } dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) } - dict[-1254192351] = { return Api.messages.WebPagePreview.parse_webPagePreview($0) } + dict[-1936029524] = { return Api.messages.WebPagePreview.parse_webPagePreview($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } + dict[-706379148] = { return Api.payments.CheckCanSendGiftResult.parse_checkCanSendGiftResultFail($0) } + dict[927967149] = { return Api.payments.CheckCanSendGiftResult.parse_checkCanSendGiftResultOk($0) } dict[675942550] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) } dict[-1730811363] = { return Api.payments.ConnectedStarRefBots.parse_connectedStarRefBots($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } @@ -1458,7 +1460,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } dict[1822222573] = { return Api.payments.StarsStatus.parse_starsStatus($0) } dict[-1261053863] = { return Api.payments.SuggestedStarRefBots.parse_suggestedStarRefBots($0) } - dict[-895289845] = { return Api.payments.UniqueStarGift.parse_uniqueStarGift($0) } + dict[1097619176] = { return Api.payments.UniqueStarGift.parse_uniqueStarGift($0) } dict[1362093126] = { return Api.payments.UniqueStarGiftValueInfo.parse_uniqueStarGiftValueInfo($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } @@ -2558,6 +2560,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.BankCardData: _1.serialize(buffer, boxed) + case let _1 as Api.payments.CheckCanSendGiftResult: + _1.serialize(buffer, boxed) case let _1 as Api.payments.CheckedGiftCode: _1.serialize(buffer, boxed) case let _1 as Api.payments.ConnectedStarRefBots: diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index d0c6d7ee81..f42d1da54f 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -123,7 +123,7 @@ public extension Api { case inputStorePaymentPremiumSubscription(flags: Int32) case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64) case inputStorePaymentStarsGiveaway(flags: Int32, stars: Int64, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) - case inputStorePaymentStarsTopup(stars: Int64, currency: String, amount: Int64) + case inputStorePaymentStarsTopup(flags: Int32, stars: Int64, currency: String, amount: Int64, spendPurposePeer: Api.InputPeer?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -221,13 +221,15 @@ public extension Api { serializeInt64(amount, buffer: buffer, boxed: false) serializeInt32(users, buffer: buffer, boxed: false) break - case .inputStorePaymentStarsTopup(let stars, let currency, let amount): + case .inputStorePaymentStarsTopup(let flags, let stars, let currency, let amount, let spendPurposePeer): if boxed { - buffer.appendInt32(-572715178) + buffer.appendInt32(-106780981) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(stars, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {spendPurposePeer!.serialize(buffer, true)} break } } @@ -248,8 +250,8 @@ public extension Api { return ("inputStorePaymentStarsGift", [("userId", userId as Any), ("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentStarsGiveaway(let flags, let stars, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount, let users): return ("inputStorePaymentStarsGiveaway", [("flags", flags as Any), ("stars", stars as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any), ("users", users as Any)]) - case .inputStorePaymentStarsTopup(let stars, let currency, let amount): - return ("inputStorePaymentStarsTopup", [("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentStarsTopup(let flags, let stars, let currency, let amount, let spendPurposePeer): + return ("inputStorePaymentStarsTopup", [("flags", flags as Any), ("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any), ("spendPurposePeer", spendPurposePeer as Any)]) } } @@ -449,17 +451,25 @@ public extension Api { } } public static func parse_inputStorePaymentStarsTopup(_ reader: BufferReader) -> InputStorePaymentPurpose? { - var _1: Int64? - _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) + var _4: Int64? + _4 = reader.readInt64() + var _5: Api.InputPeer? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.InputPeer + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputStorePaymentPurpose.inputStorePaymentStarsTopup(stars: _1!, currency: _2!, amount: _3!) + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputStorePaymentPurpose.inputStorePaymentStarsTopup(flags: _1!, stars: _2!, currency: _3!, amount: _4!, spendPurposePeer: _5) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 690efde14e..c896ec76d6 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -288,14 +288,14 @@ public extension Api { } public extension Api { enum StarGift: TypeConstructorDescription { - case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?) + case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?) case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains): + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate): if boxed { - buffer.appendInt32(12386139) + buffer.appendInt32(-2136190013) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -313,6 +313,7 @@ public extension Api { if Int(flags) & Int(1 << 6) != 0 {releasedBy!.serialize(buffer, true)} if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserTotal!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)} break case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): if boxed { @@ -349,8 +350,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains): - return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any)]) + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate): + return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any)]) case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any)]) } @@ -393,6 +394,8 @@ public extension Api { if Int(_1!) & Int(1 << 8) != 0 {_15 = reader.readInt32() } var _16: Int32? if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt32() } + var _17: Int32? + if Int(_1!) & Int(1 << 9) != 0 {_17 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -409,8 +412,9 @@ public extension Api { let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 8) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16) + let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index 1c8af79156..9d5745780a 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -812,16 +812,21 @@ public extension Api.messages { } public extension Api.messages { indirect enum WebPagePreview: TypeConstructorDescription { - case webPagePreview(media: Api.MessageMedia, users: [Api.User]) + case webPagePreview(media: Api.MessageMedia, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .webPagePreview(let media, let users): + case .webPagePreview(let media, let chats, let users): if boxed { - buffer.appendInt32(-1254192351) + buffer.appendInt32(-1936029524) } media.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) @@ -832,8 +837,8 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .webPagePreview(let media, let users): - return ("webPagePreview", [("media", media as Any), ("users", users as Any)]) + case .webPagePreview(let media, let chats, let users): + return ("webPagePreview", [("media", media as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -842,14 +847,19 @@ public extension Api.messages { if let signature = reader.readInt32() { _1 = Api.parse(reader, signature: signature) as? Api.MessageMedia } - var _2: [Api.User]? + var _2: [Api.Chat]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.WebPagePreview.webPagePreview(media: _1!, users: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.WebPagePreview.webPagePreview(media: _1!, chats: _2!, users: _3!) } else { return nil @@ -904,6 +914,56 @@ public extension Api.payments { } } +public extension Api.payments { + enum CheckCanSendGiftResult: TypeConstructorDescription { + case checkCanSendGiftResultFail(reason: Api.TextWithEntities) + case checkCanSendGiftResultOk + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .checkCanSendGiftResultFail(let reason): + if boxed { + buffer.appendInt32(-706379148) + } + reason.serialize(buffer, true) + break + case .checkCanSendGiftResultOk: + if boxed { + buffer.appendInt32(927967149) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .checkCanSendGiftResultFail(let reason): + return ("checkCanSendGiftResultFail", [("reason", reason as Any)]) + case .checkCanSendGiftResultOk: + return ("checkCanSendGiftResultOk", []) + } + } + + public static func parse_checkCanSendGiftResultFail(_ reader: BufferReader) -> CheckCanSendGiftResult? { + var _1: Api.TextWithEntities? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultFail(reason: _1!) + } + else { + return nil + } + } + public static func parse_checkCanSendGiftResultOk(_ reader: BufferReader) -> CheckCanSendGiftResult? { + return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultOk + } + + } +} public extension Api.payments { enum CheckedGiftCode: TypeConstructorDescription { case checkedGiftCode(flags: Int32, fromId: Api.Peer?, giveawayMsgId: Int32?, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User]) @@ -1552,61 +1612,3 @@ public extension Api.payments { } } -public extension Api.payments { - indirect enum PaymentResult: TypeConstructorDescription { - case paymentResult(updates: Api.Updates) - case paymentVerificationNeeded(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .paymentResult(let updates): - if boxed { - buffer.appendInt32(1314881805) - } - updates.serialize(buffer, true) - break - case .paymentVerificationNeeded(let url): - if boxed { - buffer.appendInt32(-666824391) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .paymentResult(let updates): - return ("paymentResult", [("updates", updates as Any)]) - case .paymentVerificationNeeded(let url): - return ("paymentVerificationNeeded", [("url", url as Any)]) - } - } - - public static func parse_paymentResult(_ reader: BufferReader) -> PaymentResult? { - var _1: Api.Updates? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Updates - } - let _c1 = _1 != nil - if _c1 { - return Api.payments.PaymentResult.paymentResult(updates: _1!) - } - else { - return nil - } - } - public static func parse_paymentVerificationNeeded(_ reader: BufferReader) -> PaymentResult? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.PaymentResult.paymentVerificationNeeded(url: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 1c2792b3f4..e81a788632 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -1,3 +1,61 @@ +public extension Api.payments { + indirect enum PaymentResult: TypeConstructorDescription { + case paymentResult(updates: Api.Updates) + case paymentVerificationNeeded(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentResult(let updates): + if boxed { + buffer.appendInt32(1314881805) + } + updates.serialize(buffer, true) + break + case .paymentVerificationNeeded(let url): + if boxed { + buffer.appendInt32(-666824391) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentResult(let updates): + return ("paymentResult", [("updates", updates as Any)]) + case .paymentVerificationNeeded(let url): + return ("paymentVerificationNeeded", [("url", url as Any)]) + } + } + + public static func parse_paymentResult(_ reader: BufferReader) -> PaymentResult? { + var _1: Api.Updates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Updates + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.PaymentResult.paymentResult(updates: _1!) + } + else { + return nil + } + } + public static func parse_paymentVerificationNeeded(_ reader: BufferReader) -> PaymentResult? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.PaymentResult.paymentVerificationNeeded(url: _1!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum ResaleStarGifts: TypeConstructorDescription { case resaleStarGifts(flags: Int32, count: Int32, gifts: [Api.StarGift], nextOffset: String?, attributes: [Api.StarGiftAttribute]?, attributesHash: Int64?, chats: [Api.Chat], counters: [Api.StarGiftAttributeCounter]?, users: [Api.User]) @@ -718,16 +776,21 @@ public extension Api.payments { } public extension Api.payments { enum UniqueStarGift: TypeConstructorDescription { - case uniqueStarGift(gift: Api.StarGift, users: [Api.User]) + case uniqueStarGift(gift: Api.StarGift, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .uniqueStarGift(let gift, let users): + case .uniqueStarGift(let gift, let chats, let users): if boxed { - buffer.appendInt32(-895289845) + buffer.appendInt32(1097619176) } gift.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) @@ -738,8 +801,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .uniqueStarGift(let gift, let users): - return ("uniqueStarGift", [("gift", gift as Any), ("users", users as Any)]) + case .uniqueStarGift(let gift, let chats, let users): + return ("uniqueStarGift", [("gift", gift as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -748,14 +811,19 @@ public extension Api.payments { if let signature = reader.readInt32() { _1 = Api.parse(reader, signature: signature) as? Api.StarGift } - var _2: [Api.User]? + var _2: [Api.Chat]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.payments.UniqueStarGift.uniqueStarGift(gift: _1!, users: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.UniqueStarGift.uniqueStarGift(gift: _1!, chats: _2!, users: _3!) } else { return nil @@ -1624,107 +1692,3 @@ public extension Api.premium { } } -public extension Api.smsjobs { - enum EligibilityToJoin: TypeConstructorDescription { - case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .eligibleToJoin(let termsUrl, let monthlySentSms): - if boxed { - buffer.appendInt32(-594852657) - } - serializeString(termsUrl, buffer: buffer, boxed: false) - serializeInt32(monthlySentSms, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .eligibleToJoin(let termsUrl, let monthlySentSms): - return ("eligibleToJoin", [("termsUrl", termsUrl as Any), ("monthlySentSms", monthlySentSms as Any)]) - } - } - - public static func parse_eligibleToJoin(_ reader: BufferReader) -> EligibilityToJoin? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.smsjobs.EligibilityToJoin.eligibleToJoin(termsUrl: _1!, monthlySentSms: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.smsjobs { - enum Status: TypeConstructorDescription { - case status(flags: Int32, recentSent: Int32, recentSince: Int32, recentRemains: Int32, totalSent: Int32, totalSince: Int32, lastGiftSlug: String?, termsUrl: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): - if boxed { - buffer.appendInt32(720277905) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(recentSent, buffer: buffer, boxed: false) - serializeInt32(recentSince, buffer: buffer, boxed: false) - serializeInt32(recentRemains, buffer: buffer, boxed: false) - serializeInt32(totalSent, buffer: buffer, boxed: false) - serializeInt32(totalSince, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(lastGiftSlug!, buffer: buffer, boxed: false)} - serializeString(termsUrl, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): - return ("status", [("flags", flags as Any), ("recentSent", recentSent as Any), ("recentSince", recentSince as Any), ("recentRemains", recentRemains as Any), ("totalSent", totalSent as Any), ("totalSince", totalSince as Any), ("lastGiftSlug", lastGiftSlug as Any), ("termsUrl", termsUrl as Any)]) - } - } - - public static func parse_status(_ reader: BufferReader) -> Status? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - var _7: String? - if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } - var _8: String? - _8 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.smsjobs.Status.status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index 76975e0006..324c1440e2 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,107 @@ +public extension Api.smsjobs { + enum EligibilityToJoin: TypeConstructorDescription { + case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + if boxed { + buffer.appendInt32(-594852657) + } + serializeString(termsUrl, buffer: buffer, boxed: false) + serializeInt32(monthlySentSms, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + return ("eligibleToJoin", [("termsUrl", termsUrl as Any), ("monthlySentSms", monthlySentSms as Any)]) + } + } + + public static func parse_eligibleToJoin(_ reader: BufferReader) -> EligibilityToJoin? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.smsjobs.EligibilityToJoin.eligibleToJoin(termsUrl: _1!, monthlySentSms: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.smsjobs { + enum Status: TypeConstructorDescription { + case status(flags: Int32, recentSent: Int32, recentSince: Int32, recentRemains: Int32, totalSent: Int32, totalSince: Int32, lastGiftSlug: String?, termsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + if boxed { + buffer.appendInt32(720277905) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(recentSent, buffer: buffer, boxed: false) + serializeInt32(recentSince, buffer: buffer, boxed: false) + serializeInt32(recentRemains, buffer: buffer, boxed: false) + serializeInt32(totalSent, buffer: buffer, boxed: false) + serializeInt32(totalSince, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(lastGiftSlug!, buffer: buffer, boxed: false)} + serializeString(termsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + return ("status", [("flags", flags as Any), ("recentSent", recentSent as Any), ("recentSince", recentSince as Any), ("recentRemains", recentRemains as Any), ("totalSent", totalSent as Any), ("totalSince", totalSince as Any), ("lastGiftSlug", lastGiftSlug as Any), ("termsUrl", termsUrl as Any)]) + } + } + + public static func parse_status(_ reader: BufferReader) -> Status? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.smsjobs.Status.status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!) + } + else { + return nil + } + } + + } +} public extension Api.stats { enum BroadcastStats: TypeConstructorDescription { case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, reactionsPerPost: Api.StatsAbsValueAndPrev, viewsPerStory: Api.StatsAbsValueAndPrev, sharesPerStory: Api.StatsAbsValueAndPrev, reactionsPerStory: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph, storyInteractionsGraph: Api.StatsGraph, storyReactionsByEmotionGraph: Api.StatsGraph, recentPostsInteractions: [Api.PostInteractionCounters]) @@ -1450,207 +1554,3 @@ public extension Api.updates { } } -public extension Api.updates { - enum Difference: TypeConstructorDescription { - case difference(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) - case differenceEmpty(date: Int32, seq: Int32) - case differenceSlice(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], intermediateState: Api.updates.State) - case differenceTooLong(pts: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .difference(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let state): - if boxed { - buffer.appendInt32(16030880) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newMessages.count)) - for item in newMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newEncryptedMessages.count)) - for item in newEncryptedMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(otherUpdates.count)) - for item in otherUpdates { - 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) - } - state.serialize(buffer, true) - break - case .differenceEmpty(let date, let seq): - if boxed { - buffer.appendInt32(1567990072) - } - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(seq, buffer: buffer, boxed: false) - break - case .differenceSlice(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let intermediateState): - if boxed { - buffer.appendInt32(-1459938943) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newMessages.count)) - for item in newMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newEncryptedMessages.count)) - for item in newEncryptedMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(otherUpdates.count)) - for item in otherUpdates { - 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) - } - intermediateState.serialize(buffer, true) - break - case .differenceTooLong(let pts): - if boxed { - buffer.appendInt32(1258196845) - } - serializeInt32(pts, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .difference(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let state): - return ("difference", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("state", state as Any)]) - case .differenceEmpty(let date, let seq): - return ("differenceEmpty", [("date", date as Any), ("seq", seq as Any)]) - case .differenceSlice(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let intermediateState): - return ("differenceSlice", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("intermediateState", intermediateState as Any)]) - case .differenceTooLong(let pts): - return ("differenceTooLong", [("pts", pts as Any)]) - } - } - - public static func parse_difference(_ reader: BufferReader) -> Difference? { - var _1: [Api.Message]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _2: [Api.EncryptedMessage]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EncryptedMessage.self) - } - var _3: [Api.Update]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _6: Api.updates.State? - if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.updates.State - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.updates.Difference.difference(newMessages: _1!, newEncryptedMessages: _2!, otherUpdates: _3!, chats: _4!, users: _5!, state: _6!) - } - else { - return nil - } - } - public static func parse_differenceEmpty(_ reader: BufferReader) -> Difference? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.updates.Difference.differenceEmpty(date: _1!, seq: _2!) - } - else { - return nil - } - } - public static func parse_differenceSlice(_ reader: BufferReader) -> Difference? { - var _1: [Api.Message]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _2: [Api.EncryptedMessage]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EncryptedMessage.self) - } - var _3: [Api.Update]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _6: Api.updates.State? - if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.updates.State - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.updates.Difference.differenceSlice(newMessages: _1!, newEncryptedMessages: _2!, otherUpdates: _3!, chats: _4!, users: _5!, intermediateState: _6!) - } - else { - return nil - } - } - public static func parse_differenceTooLong(_ reader: BufferReader) -> Difference? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.updates.Difference.differenceTooLong(pts: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 01f3f0e833..0b6b16c01e 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,207 @@ +public extension Api.updates { + enum Difference: TypeConstructorDescription { + case difference(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) + case differenceEmpty(date: Int32, seq: Int32) + case differenceSlice(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], intermediateState: Api.updates.State) + case differenceTooLong(pts: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .difference(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let state): + if boxed { + buffer.appendInt32(16030880) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newMessages.count)) + for item in newMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newEncryptedMessages.count)) + for item in newEncryptedMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(otherUpdates.count)) + for item in otherUpdates { + 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) + } + state.serialize(buffer, true) + break + case .differenceEmpty(let date, let seq): + if boxed { + buffer.appendInt32(1567990072) + } + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(seq, buffer: buffer, boxed: false) + break + case .differenceSlice(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let intermediateState): + if boxed { + buffer.appendInt32(-1459938943) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newMessages.count)) + for item in newMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newEncryptedMessages.count)) + for item in newEncryptedMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(otherUpdates.count)) + for item in otherUpdates { + 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) + } + intermediateState.serialize(buffer, true) + break + case .differenceTooLong(let pts): + if boxed { + buffer.appendInt32(1258196845) + } + serializeInt32(pts, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .difference(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let state): + return ("difference", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("state", state as Any)]) + case .differenceEmpty(let date, let seq): + return ("differenceEmpty", [("date", date as Any), ("seq", seq as Any)]) + case .differenceSlice(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let intermediateState): + return ("differenceSlice", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("intermediateState", intermediateState as Any)]) + case .differenceTooLong(let pts): + return ("differenceTooLong", [("pts", pts as Any)]) + } + } + + public static func parse_difference(_ reader: BufferReader) -> Difference? { + var _1: [Api.Message]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _2: [Api.EncryptedMessage]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EncryptedMessage.self) + } + var _3: [Api.Update]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: Api.updates.State? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.updates.State + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.updates.Difference.difference(newMessages: _1!, newEncryptedMessages: _2!, otherUpdates: _3!, chats: _4!, users: _5!, state: _6!) + } + else { + return nil + } + } + public static func parse_differenceEmpty(_ reader: BufferReader) -> Difference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.updates.Difference.differenceEmpty(date: _1!, seq: _2!) + } + else { + return nil + } + } + public static func parse_differenceSlice(_ reader: BufferReader) -> Difference? { + var _1: [Api.Message]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _2: [Api.EncryptedMessage]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EncryptedMessage.self) + } + var _3: [Api.Update]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: Api.updates.State? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.updates.State + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.updates.Difference.differenceSlice(newMessages: _1!, newEncryptedMessages: _2!, otherUpdates: _3!, chats: _4!, users: _5!, intermediateState: _6!) + } + else { + return nil + } + } + public static func parse_differenceTooLong(_ reader: BufferReader) -> Difference? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.updates.Difference.differenceTooLong(pts: _1!) + } + else { + return nil + } + } + + } +} public extension Api.updates { enum State: TypeConstructorDescription { case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 7ea798e826..c9bfe5f2df 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -9362,6 +9362,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func checkCanSendGift(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1060835895) + serializeInt64(giftId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.checkCanSendGift", parameters: [("giftId", String(describing: giftId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.CheckCanSendGiftResult? in + let reader = BufferReader(buffer) + var result: Api.payments.CheckCanSendGiftResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.CheckCanSendGiftResult + } + return result + }) + } +} public extension Api.functions.payments { static func checkGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -12323,6 +12338,26 @@ public extension Api.functions.users { }) } } +public extension Api.functions.users { + static func getSavedMusicByID(id: Api.InputUser, documents: [Api.InputDocument]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1970513129) + id.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents.count)) + for item in documents { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "users.getSavedMusicByID", parameters: [("id", String(describing: id)), ("documents", String(describing: documents))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.users.SavedMusic? in + let reader = BufferReader(buffer) + var result: Api.users.SavedMusic? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.users.SavedMusic + } + return result + }) + } +} public extension Api.functions.users { static func getUsers(id: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.User]>) { let buffer = Buffer() diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 22d3b36964..b543d3f8fa 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -860,7 +860,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: strongSelf.navigationController as? NavigationController) + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: strongSelf.navigationController as? NavigationController, updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { @@ -901,7 +901,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.navigationController as? NavigationController) + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.navigationController as? NavigationController, updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if index.1 { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 65bcab65af..d8765c1825 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -191,7 +191,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId)) + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0)) case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate): guard let gift = StarGift(apiStarGift: apiGift) else { return nil diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 2cc69b8557..40fd07692b 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -334,6 +334,7 @@ private enum MediaReferenceRevalidationKey: Hashable { case customEmoji(fileId: Int64) case story(peer: PeerReference, id: Int32) case starsTransaction(transaction: StarsTransactionReference) + case savedMusic(peer: PeerReference, fileId: Int64) } private final class MediaReferenceRevalidationItemContext { @@ -794,6 +795,33 @@ final class MediaReferenceRevalidationContext { } } + func savedMusic(accountPeerId: PeerId, postbox: Postbox, network: Network, background: Bool, peer: PeerReference, media: Media) -> Signal { + guard let file = media as? TelegramMediaFile else { + return .fail(.generic) + } + return self.genericItem(key: .savedMusic(peer: peer, fileId: file.fileId.id), background: background, request: { next, error in + return (_internal_getSavedMusicById(postbox: postbox, network: network, peer: peer, file: file) + |> castError(RevalidateMediaReferenceError.self) + |> mapToSignal { result -> Signal in + if let result { + return .single(result) + } else { + return .fail(.generic) + } + }).start(next: { value in + next(value) + }, error: { _ in + error(.generic) + }) + }) |> mapToSignal { next -> Signal in + if let next = next as? TelegramMediaFile { + return .single(next) + } else { + return .fail(.generic) + } + } + } + func notificationSoundList(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> { return self.genericItem(key: .notificationSoundList, background: background, request: { next, error in return (requestNotificationSoundList(network: network, hash: 0) @@ -993,6 +1021,14 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n } return .fail(.generic) } + case let .savedMusic(peer, media): + return revalidationContext.savedMusic(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer, media: media) + |> mapToSignal { result -> Signal in + if let updatedResource = findUpdatedMediaResource(media: result, previousMedia: media, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + return .fail(.generic) + } case let .standalone(media): if let file = media as? TelegramMediaFile { for attribute in file.attributes { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 5e884f3921..78362448e0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -1089,6 +1089,15 @@ public final class CachedUserData: CachedPeerData { public enum LinkedBotChannelId: Equatable { case unknown case known(PeerId?) + + public var value: PeerId? { + switch self { + case .unknown: + return nil + case let .known(peerId): + return peerId + } + } } public let about: String? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index d0f1e0a6bc..d94f674edb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -277,6 +277,7 @@ public enum AnyMediaReference: Equatable { case customEmoji(media: Media) case story(peer: PeerReference, id: Int32, media: Media) case starsTransaction(transaction: StarsTransactionReference, media: Media) + case savedMusic(peer: PeerReference, media: Media) public static func ==(lhs: AnyMediaReference, rhs: AnyMediaReference) -> Bool { switch lhs { @@ -352,6 +353,12 @@ public enum AnyMediaReference: Equatable { } else { return false } + case let .savedMusic(lhsPeer, lhsMedia): + if case let .savedMusic(rhsPeer, rhsMedia) = rhs, lhsPeer == rhsPeer, lhsMedia.isEqual(to: rhsMedia) { + return true + } else { + return false + } } } @@ -381,6 +388,8 @@ public enum AnyMediaReference: Equatable { return nil case .starsTransaction: return nil + case .savedMusic: + return nil } } @@ -434,6 +443,10 @@ public enum AnyMediaReference: Equatable { if let media = media as? T { return .starsTransaction(transaction: transaction, media: media) } + case let .savedMusic(peer, media): + if let media = media as? T { + return .savedMusic(peer: peer, media: media) + } } return nil } @@ -464,6 +477,8 @@ public enum AnyMediaReference: Equatable { return media case let .starsTransaction(_, media): return media + case let .savedMusic(_, media): + return media } } @@ -493,6 +508,8 @@ public enum AnyMediaReference: Equatable { return .story(peer: peer, id: id, media: media) case let .starsTransaction(transaction, _): return .starsTransaction(transaction: transaction, media: media) + case let .savedMusic(peer, _): + return .savedMusic(peer: peer, media: media) } } @@ -593,6 +610,7 @@ public enum MediaReference { case customEmoji case story case starsTransaction + case savedMusic } case standalone(media: T) @@ -607,6 +625,7 @@ public enum MediaReference { case customEmoji(media: T) case story(peer: PeerReference, id: Int32, media: T) case starsTransaction(transaction: StarsTransactionReference, media: T) + case savedMusic(peer: PeerReference, media: T) public init?(decoder: PostboxDecoder) { guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else { @@ -681,6 +700,12 @@ public enum MediaReference { return nil } self = .starsTransaction(transaction: transaction, media: media) + case .savedMusic: + let peer = decoder.decodeObjectForKey("pr", decoder: { PeerReference(decoder: $0) }) as! PeerReference + guard let media = decoder.decodeObjectForKey("m") as? T else { + return nil + } + self = .savedMusic(peer: peer, media: media) } } @@ -730,9 +755,12 @@ public enum MediaReference { encoder.encodeInt32(CodingCase.starsTransaction.rawValue, forKey: "_r") encoder.encodeObject(transaction, forKey: "tr") encoder.encodeObject(media, forKey: "m") + case let .savedMusic(peer, media): + encoder.encodeInt32(CodingCase.savedMusic.rawValue, forKey: "_r") + encoder.encodeObject(peer, forKey: "pr") + encoder.encodeObject(media, forKey: "m") } } - public var abstract: AnyMediaReference { switch self { @@ -760,6 +788,8 @@ public enum MediaReference { return .story(peer: peer, id: id, media: media) case let .starsTransaction(transaction, media): return .starsTransaction(transaction: transaction, media: media) + case let .savedMusic(peer, media): + return .savedMusic(peer: peer, media: media) } } @@ -793,6 +823,8 @@ public enum MediaReference { return media case let .starsTransaction(_, media): return media + case let .savedMusic(_, media): + return media } } @@ -822,6 +854,8 @@ public enum MediaReference { return .story(peer: peer, id: id, media: media) case let .starsTransaction(transaction, _): return .starsTransaction(transaction: transaction, media: media) + case let .savedMusic(peer, _): + return .savedMusic(peer: peer, media: media) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 65d1c8835e..5e105452ed 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -243,7 +243,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) - case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?) + case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) @@ -375,7 +375,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) case 44: - self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId")) + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false) case 45: var resaleAmount: CurrencyAmount? if let amount = decoder.decodeCodable(CurrencyAmount.self, forKey: "resaleAmount") { @@ -705,7 +705,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate): encoder.encodeInt32(44, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") if let convertStars { @@ -762,6 +762,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giftMessageId") } + encoder.encodeBool(upgradeSeparate, forKey: "upgradeSeparate") case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") @@ -887,7 +888,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return [peerId] case let .prizeStars(_, _, boostPeerId, _, _): return boostPeerId.flatMap { [$0] } ?? [] - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 43932cf03a..ccbf201a4a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -2415,6 +2415,36 @@ public extension TelegramEngine.EngineData.Item { } } + public struct ProfileMainTab: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.mainProfileTab + } else if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.mainProfileTab + } else { + return nil + } + } + } + public struct BotPrivacyPolicyUrl: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Optional diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index c89af92e93..9276796244 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -17,7 +17,7 @@ public enum AppStoreTransactionPurpose { case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) - case stars(count: Int64, currency: String, amount: Int64) + case stars(count: Int64, currency: String, amount: Int64, peerId: EnginePeer.Id?) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) @@ -99,10 +99,26 @@ private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTran return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)) } |> switchToLatest - case let .stars(count, currency, amount): - return .single(.inputStorePaymentStarsTopup(stars: count, currency: currency, amount: amount)) + case let .stars(count, currency, amount, peerId): + let peerSignal: Signal + if let peerId { + peerSignal = postbox.loadedPeerWithId(peerId) + |> map { peer in + return apiInputPeer(peer) + } + } else { + peerSignal = .single(nil) + } + return peerSignal + |> map { spendPurposePeer in + var flags: Int32 = 0 + if let _ = spendPurposePeer { + flags |= (1 << 0) + } + return .inputStorePaymentStarsTopup(flags: flags, stars: count, currency: currency, amount: amount, spendPurposePeer: spendPurposePeer) + } case let .starsGift(peerId, count, currency, amount): - return postbox.loadedPeerWithId(peerId) + return postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in guard let inputUser = apiInputUser(peer) else { return .complete() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 919c4079f2..11194a5e51 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -9,7 +9,7 @@ public enum BotPaymentInvoiceSource { case slug(String) case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption, text: String?, entities: [MessageTextEntity]?) - case stars(option: StarsTopUpOption) + case stars(option: StarsTopUpOption, peerId: EnginePeer.Id?) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsChatSubscription(hash: String) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) @@ -325,12 +325,14 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) - case let .stars(option): + case let .stars(option, peerId): var flags: Int32 = 0 - if let _ = option.storeProductId { + var spendPurposePeer: Api.InputPeer? + if let peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { flags |= (1 << 0) + spendPurposePeer = inputPeer } - return .inputInvoiceStars(purpose: .inputStorePaymentStarsTopup(stars: option.count, currency: option.currency, amount: option.amount)) + return .inputInvoiceStars(purpose: .inputStorePaymentStarsTopup(flags: flags, stars: option.count, currency: option.currency, amount: option.amount, spendPurposePeer: spendPurposePeer)) case let .starsGift(peerId, count, currency, amount): guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else { return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index e3f261b9e9..dc20722766 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -57,6 +57,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case upgradeStars case releasedBy case perUserLimit + case lockedUntilDate } public struct Availability: Equatable, Codable, PostboxCoding { @@ -179,8 +180,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let upgradeStars: Int64? public let releasedBy: EnginePeer.Id? public let perUserLimit: PerUserLimit? + public let lockedUntilDate: Int32? - public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?) { + public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?, lockedUntilDate: Int32?) { self.id = id self.title = title self.file = file @@ -192,6 +194,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.upgradeStars = upgradeStars self.releasedBy = releasedBy self.perUserLimit = perUserLimit + self.lockedUntilDate = lockedUntilDate } public init(from decoder: Decoder) throws { @@ -213,6 +216,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars) self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy) self.perUserLimit = try container.decodeIfPresent(PerUserLimit.self, forKey: .perUserLimit) + self.lockedUntilDate = try container.decodeIfPresent(Int32.self, forKey: .lockedUntilDate) } public init(decoder: PostboxDecoder) { @@ -227,6 +231,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.upgradeStars = decoder.decodeOptionalInt64ForKey(CodingKeys.upgradeStars.rawValue) self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) } self.perUserLimit = decoder.decodeObjectForKey(CodingKeys.perUserLimit.rawValue, decoder: { StarGift.Gift.PerUserLimit(decoder: $0) }) as? StarGift.Gift.PerUserLimit + self.lockedUntilDate = decoder.decodeOptionalInt32ForKey(CodingKeys.lockedUntilDate.rawValue) } public func encode(to encoder: Encoder) throws { @@ -247,6 +252,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars) try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy) try container.encodeIfPresent(self.perUserLimit, forKey: .perUserLimit) + try container.encodeIfPresent(self.lockedUntilDate, forKey: .lockedUntilDate) } public func encode(_ encoder: PostboxEncoder) { @@ -285,6 +291,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding { } else { encoder.encodeNil(forKey: CodingKeys.perUserLimit.rawValue) } + if let lockedUntilDate = self.lockedUntilDate { + encoder.encodeInt32(lockedUntilDate, forKey: CodingKeys.lockedUntilDate.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.lockedUntilDate.rawValue) + } } } @@ -843,7 +854,7 @@ public extension StarGift { extension StarGift { init?(apiStarGift: Api.StarGift) { switch apiStarGift { - case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains): + case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains, lockedUntilDate): var flags = StarGift.Gift.Flags() if (apiFlags & (1 << 2)) != 0 { flags.insert(.isBirthdayGift) @@ -872,7 +883,7 @@ extension StarGift { guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { return nil } - self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit)) + self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate)) case let .starGiftUnique(flags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency): let owner: StarGift.UniqueGift.Owner if let ownerAddress { @@ -1182,7 +1193,8 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star canTransferDate: canTransferDate, canResaleDate: canResaleDate, collectionIds: nil, - prepaidUpgradeHash: nil + prepaidUpgradeHash: nil, + upgradeSeparate: false )) } } @@ -1214,6 +1226,34 @@ func _internal_starGiftUpgradePreview(account: Account, giftId: Int64) -> Signal } } +public enum CanSendGiftResult { + case available + case unavailable(text: String, entities: [MessageTextEntity]) + case failed +} + +func _internal_checkCanSendStarGift(account: Account, giftId: Int64) -> Signal { + return account.network.request(Api.functions.payments.checkCanSendGift(giftId: giftId)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result in + guard let result else { + return .unavailable(text: "", entities: []) + } + switch result { + case .checkCanSendGiftResultOk: + return .available + case let .checkCanSendGiftResultFail(reason): + switch reason { + case let .textWithEntities(text, entities): + return .unavailable(text: text, entities: messageTextEntitiesFromApiEntities(entities)) + } + } + } +} + final class CachedProfileGifts: Codable { enum CodingKeys: String, CodingKey { case gifts @@ -2037,6 +2077,7 @@ public final class ProfileGiftsContext { case canResaleDate case collectionIds case prepaidUpgradeHash + case upgradeSeparate } public let gift: TelegramCore.StarGift @@ -2057,6 +2098,7 @@ public final class ProfileGiftsContext { public let canResaleDate: Int32? public let collectionIds: [Int32]? public let prepaidUpgradeHash: String? + public let upgradeSeparate: Bool fileprivate let _fromPeerId: EnginePeer.Id? @@ -2082,7 +2124,8 @@ public final class ProfileGiftsContext { canTransferDate: Int32?, canResaleDate: Int32?, collectionIds: [Int32]?, - prepaidUpgradeHash: String? + prepaidUpgradeHash: String?, + upgradeSeparate: Bool ) { self.gift = gift self.reference = reference @@ -2103,6 +2146,7 @@ public final class ProfileGiftsContext { self.canResaleDate = canResaleDate self.collectionIds = collectionIds self.prepaidUpgradeHash = prepaidUpgradeHash + self.upgradeSeparate = upgradeSeparate } public init(from decoder: Decoder) throws { @@ -2133,6 +2177,7 @@ public final class ProfileGiftsContext { self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate) self.collectionIds = try container.decodeIfPresent([Int32].self, forKey: .collectionIds) self.prepaidUpgradeHash = try container.decodeIfPresent(String.self, forKey: .prepaidUpgradeHash) + self.upgradeSeparate = try container.decodeIfPresent(Bool.self, forKey: .upgradeSeparate) ?? false } public func encode(to encoder: Encoder) throws { @@ -2156,6 +2201,7 @@ public final class ProfileGiftsContext { try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate) try container.encodeIfPresent(self.collectionIds, forKey: .collectionIds) try container.encodeIfPresent(self.prepaidUpgradeHash, forKey: .prepaidUpgradeHash) + try container.encode(self.upgradeSeparate, forKey: .upgradeSeparate) } public func withGift(_ gift: TelegramCore.StarGift) -> StarGift { @@ -2177,7 +2223,8 @@ public final class ProfileGiftsContext { canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate, collectionIds: self.collectionIds, - prepaidUpgradeHash: self.prepaidUpgradeHash + prepaidUpgradeHash: self.prepaidUpgradeHash, + upgradeSeparate: self.upgradeSeparate ) } @@ -2200,7 +2247,8 @@ public final class ProfileGiftsContext { canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate, collectionIds: self.collectionIds, - prepaidUpgradeHash: self.prepaidUpgradeHash + prepaidUpgradeHash: self.prepaidUpgradeHash, + upgradeSeparate: self.upgradeSeparate ) } @@ -2223,7 +2271,8 @@ public final class ProfileGiftsContext { canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate, collectionIds: self.collectionIds, - prepaidUpgradeHash: self.prepaidUpgradeHash + prepaidUpgradeHash: self.prepaidUpgradeHash, + upgradeSeparate: self.upgradeSeparate ) } fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift { @@ -2245,7 +2294,8 @@ public final class ProfileGiftsContext { canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate, collectionIds: self.collectionIds, - prepaidUpgradeHash: self.prepaidUpgradeHash + prepaidUpgradeHash: self.prepaidUpgradeHash, + upgradeSeparate: self.upgradeSeparate ) } @@ -2268,7 +2318,8 @@ public final class ProfileGiftsContext { canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate, collectionIds: collectionIds, - prepaidUpgradeHash: self.prepaidUpgradeHash + prepaidUpgradeHash: self.prepaidUpgradeHash, + upgradeSeparate: self.upgradeSeparate ) } } @@ -2519,6 +2570,7 @@ extension ProfileGiftsContext.State.StarGift { self.canResaleDate = canResaleAt self.collectionIds = collectionIds self.prepaidUpgradeHash = prepaidUpgradeHash + self.upgradeSeparate = (flags & (1 << 17)) != 0 } } } @@ -2560,9 +2612,9 @@ func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal mapToSignal { result -> Signal in if let result = result { switch result { - case let .uniqueStarGift(gift, users): + case let .uniqueStarGift(gift, chats, users): return account.postbox.transaction { transaction in - let parsedPeers = AccumulatedPeers(users: users) + let parsedPeers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) guard case let .unique(uniqueGift) = StarGift(apiStarGift: gift) else { return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 4b5cbb69d3..f2c19cb801 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1654,7 +1654,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot canTransferDate: canTransferDate, canResaleDate: canResaleDate, collectionIds: nil, - prepaidUpgradeHash: nil + prepaidUpgradeHash: nil, + upgradeSeparate: false ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index dc4e9f10e7..bfbd3d48b2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -141,6 +141,10 @@ public extension TelegramEngine { return _internal_starGiftUpgradePreview(account: self.account, giftId: giftId) } + public func checkCanSendStarGift(giftId: Int64) -> Signal { + return _internal_checkCanSendStarGift(account: self.account, giftId: giftId) + } + public func getUniqueStarGift(slug: String) -> Signal { return _internal_getUniqueStarGift(account: self.account, slug: slug) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift index f2fcbd21a9..5e0cb2082c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift @@ -49,6 +49,31 @@ public final class SavedMusicIdsList: Codable, Equatable { } } +func _internal_getSavedMusicById(postbox: Postbox, network: Network, peer: PeerReference, file: TelegramMediaFile) -> Signal { + let inputUser = peer.inputUser + guard let inputUser, let resource = file.resource as? CloudDocumentMediaResource else { + return .single(nil) + } + return network.request(Api.functions.users.getSavedMusicByID(id: inputUser, documents: [.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))])) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> TelegramMediaFile? in + if let result { + switch result { + case let .savedMusic(_, documents): + if let file = documents.first.flatMap({ telegramMediaFileFromApiDocument($0, altDocuments: nil) }) { + return file + } + default: + break + } + } + return nil + } +} + func _internal_savedMusicIds(postbox: Postbox) -> Signal?, NoError> { let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.savedMusicIds()])) return postbox.combinedView(keys: [viewKey]) @@ -102,6 +127,7 @@ func _internal_addSavedMusic(account: Account, file: FileMediaReference, afterFi return account.postbox.transaction { transaction in if let cachedSavedMusic = transaction.retrieveItemCacheEntry(id: entryId(peerId: account.peerId))?.get(CachedProfileSavedMusic.self) { var updatedFiles = cachedSavedMusic.files + updatedFiles.removeAll(where: { $0.fileId == file.media.fileId }) if let afterFile, let index = updatedFiles.firstIndex(where: { $0.fileId == afterFile.media.fileId }) { updatedFiles.insert(file.media, at: index + 1) } else { @@ -119,15 +145,17 @@ func _internal_addSavedMusic(account: Account, file: FileMediaReference, afterFi transaction.setPreferencesEntry(key: PreferencesKeys.savedMusicIds(), value: PreferencesEntry(savedMusicIdsList)) } - transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, cachedData -> CachedPeerData? in - if let cachedData = cachedData as? CachedUserData { - var updatedData = cachedData - updatedData = updatedData.withUpdatedSavedMusic(file.media) - return updatedData - } else { - return cachedData - } - }) + if afterFile == nil { + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedUserData { + var updatedData = cachedData + updatedData = updatedData.withUpdatedSavedMusic(file.media) + return updatedData + } else { + return cachedData + } + }) + } } return revalidatedMusic(account: account, file: file, signal: { resource in var flags: Int32 = 0 @@ -258,7 +286,7 @@ public final class ProfileSavedMusicContext { self.account = account self.peerId = peerId - self.reload() + self.loadMore() } deinit { @@ -342,36 +370,33 @@ public final class ProfileSavedMusicContext { })) } - public func addMusic(file: FileMediaReference, afterFile: FileMediaReference? = nil) -> Signal { - return _internal_addSavedMusic(account: self.account, file: file, afterFile: nil) - |> afterCompleted { [weak self] in - guard let self else { - return - } - if let afterFile, let index = self.files.firstIndex(where: { $0.fileId == afterFile.media.fileId }) { - self.files.insert(file.media, at: index + 1) - } else { - self.files.insert(file.media, at: 0) - } - if let count = self.count { - self.count = count + 1 - } - self.pushState() + public func addMusic(file: FileMediaReference, afterFile: FileMediaReference? = nil, apply: Bool = true) -> Signal { + self.files.removeAll(where: { $0.fileId == file.media.fileId }) + if let afterFile, let index = self.files.firstIndex(where: { $0.fileId == afterFile.media.fileId }) { + self.files.insert(file.media, at: index + 1) + } else { + self.files.insert(file.media, at: 0) + } + if let count = self.count { + self.count = count + 1 + } + self.pushState() + + if apply { + return _internal_addSavedMusic(account: self.account, file: file, afterFile: afterFile) + } else { + return .complete() } } public func removeMusic(file: FileMediaReference) -> Signal { - return _internal_removeSavedMusic(account: self.account, file: file) - |> afterCompleted { [weak self] in - guard let self else { - return - } - self.files.removeAll(where: { $0.fileId == file.media.id }) - if let count = self.count { - self.count = max(0, count - 1) - } - self.pushState() + self.files.removeAll(where: { $0.fileId == file.media.id }) + if let count = self.count { + self.count = max(0, count - 1) } + self.pushState() + + return _internal_removeSavedMusic(account: self.account, file: file) } private func pushState() { diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index 7109d300d1..dbc8440822 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -157,7 +157,7 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage return account.network.requestWithAdditionalInfo(Api.functions.messages.getWebPagePreview(flags: 0, message: urls.joined(separator: " "), entities: nil), info: .progress) |> `catch` { _ -> Signal, NoError> in - return .single(.result(.webPagePreview(media: .messageMediaEmpty, users: []))) + return .single(.result(.webPagePreview(media: .messageMediaEmpty, chats: [], users: []))) } |> mapToSignal { result -> Signal in switch result { @@ -170,17 +170,17 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage return .complete() } case let .result(result): - if case let .webPagePreview(result, _) = result, let preCachedResources = result.preCachedResources { + if case let .webPagePreview(result, _, _) = result, let preCachedResources = result.preCachedResources { for (resource, data) in preCachedResources { account.postbox.mediaBox.storeResourceData(resource.id, data: data) } } switch result { - case let .webPagePreview(media, users): + case let .webPagePreview(media, chats, users): switch media { case let .messageMediaWebPage(_, webpage): return account.postbox.transaction { transaction -> Signal in - let peers = AccumulatedPeers(users: users) + let peers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) if let media = telegramMediaWebpageFromApiWebpage(webpage), let url = media.content.url { diff --git a/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift index 29f0d81dcc..d26866caad 100644 --- a/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift +++ b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift @@ -9,6 +9,27 @@ private let animationDelay: TimeInterval = 2.5 private let spacing: CGFloat = 20.0 public final class MarqueeComponent: Component { + let attributedText: NSAttributedString + let maxWidth: CGFloat? + + public init( + attributedText: NSAttributedString, + maxWidth: CGFloat? = nil + ) { + self.attributedText = attributedText + self.maxWidth = maxWidth + } + + public static func ==(lhs: MarqueeComponent, rhs: MarqueeComponent) -> Bool { + if lhs.attributedText != rhs.attributedText { + return false + } + if lhs.maxWidth != rhs.maxWidth { + return false + } + return true + } + public static let innerPadding: CGFloat = 16.0 private final class MeasureState: Equatable { @@ -41,7 +62,9 @@ public final class MarqueeComponent: Component { private let containerLayer = SimpleLayer() private let textLayer = SimpleLayer() private let duplicateTextLayer = SimpleLayer() + private let maskContainerLayer = SimpleLayer() private let gradientMaskLayer = SimpleGradientLayer() + private let solidEdgeMaskLayer = SimpleLayer() private var isAnimating = false private var isOverflowing = false @@ -56,6 +79,9 @@ public final class MarqueeComponent: Component { self.containerLayer.addSublayer(self.textLayer) self.containerLayer.addSublayer(self.duplicateTextLayer) + + self.maskContainerLayer.addSublayer(self.gradientMaskLayer) + self.maskContainerLayer.addSublayer(self.solidEdgeMaskLayer) } required init?(coder: NSCoder) { @@ -66,6 +92,11 @@ public final class MarqueeComponent: Component { let previousComponent = self.component self.component = component + var availableSize = availableSize + if let maxWidth = component.maxWidth { + availableSize.width = maxWidth + } + let attributedText = component.attributedText if let measureState = self.measureState { if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize { @@ -97,27 +128,34 @@ public final class MarqueeComponent: Component { self.startAnimation(force: previousComponent?.attributedText != attributedText) } else { self.stopAnimation() - self.textLayer.frame = CGRect(origin: CGPoint(x: innerPadding, y: 0.0), size: boundingRect.size) + self.textLayer.frame = CGRect(origin: .zero, size: boundingRect.size) self.textLayer.contents = image.cgImage self.duplicateTextLayer.frame = .zero self.duplicateTextLayer.contents = nil self.layer.mask = nil } - return CGSize(width: min(measureState.size.width + innerPadding * 2.0, availableSize.width), height: measureState.size.height) + return CGSize(width: min(measureState.size.width, availableSize.width), height: measureState.size.height) } private func setupMarqueeTextLayers(textImage: CGImage, textWidth: CGFloat, containerWidth: CGFloat) { - self.textLayer.frame = CGRect(x: innerPadding, y: 0, width: textWidth, height: self.containerLayer.bounds.height) + self.textLayer.frame = CGRect(x: 0.0, y: 0, width: textWidth, height: self.containerLayer.bounds.height) self.textLayer.contents = textImage - self.duplicateTextLayer.frame = CGRect(x: innerPadding + textWidth + spacing, y: 0, width: textWidth, height: self.containerLayer.bounds.height) + self.duplicateTextLayer.frame = CGRect(x: textWidth + spacing, y: 0, width: textWidth, height: self.containerLayer.bounds.height) self.duplicateTextLayer.contents = textImage self.containerLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: textWidth * 2.0 + spacing, height: self.containerLayer.bounds.height)) } private func setupGradientMask(size: CGSize) { + let edgePercentage = innerPadding / size.width + + self.maskContainerLayer.frame = CGRect(origin: .zero, size: size) + + self.solidEdgeMaskLayer.frame = CGRect(origin: .zero, size: CGSize(width: innerPadding, height: size.height)) + self.solidEdgeMaskLayer.backgroundColor = UIColor.black.cgColor + self.gradientMaskLayer.frame = CGRect(origin: .zero, size: size) self.gradientMaskLayer.colors = [ UIColor.clear.cgColor, @@ -125,22 +163,20 @@ public final class MarqueeComponent: Component { UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor, - UIColor.clear.cgColor + UIColor.clear.cgColor, ] self.gradientMaskLayer.startPoint = CGPoint(x: 0.0, y: 0.5) self.gradientMaskLayer.endPoint = CGPoint(x: 1.0, y: 0.5) - - let edgePercentage = innerPadding / size.width self.gradientMaskLayer.locations = [ 0.0, - NSNumber(value: edgePercentage * 0.4), + NSNumber(value: edgePercentage * 0.1), NSNumber(value: edgePercentage), NSNumber(value: 1.0 - edgePercentage), - NSNumber(value: 1.0 - edgePercentage * 0.4), + NSNumber(value: 1.0 - edgePercentage * 0.1), 1.0 ] - - self.layer.mask = self.gradientMaskLayer + + self.layer.mask = self.maskContainerLayer } private func startAnimation(force: Bool = false) { @@ -157,6 +193,14 @@ public final class MarqueeComponent: Component { guard self.isAnimating else { return } + let values: [NSNumber] = [1.0, 0.0, 0.0, 1.0] + let keyTimes: [NSNumber] = [0.0, 0.02, 0.98, 1.0] + self.solidEdgeMaskLayer.animateKeyframes( + values: values, + keyTimes: keyTimes, + duration: duration, + keyPath: "opacity" + ) self.containerLayer.animateBoundsOriginXAdditive(from: 0.0, to: distance, duration: duration, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, completion: { finished in if finished { self.isAnimating = false @@ -171,20 +215,7 @@ public final class MarqueeComponent: Component { self.isAnimating = false } } - - public let attributedText: NSAttributedString - - public init(attributedText: NSAttributedString) { - self.attributedText = attributedText - } - - public static func ==(lhs: MarqueeComponent, rhs: MarqueeComponent) -> Bool { - if lhs.attributedText != rhs.attributedText { - return false - } - return true - } - + public func makeView() -> View { return View() } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift index 41133017a6..3da3eb6fe2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift @@ -406,7 +406,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } - let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.chatControllerInteraction.navigationController()) + let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.chatControllerInteraction.navigationController(), updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.view.window?.endEditing(true) strongSelf.chatControllerInteraction.presentController(controller, nil) } else if index.1 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index dba35f006c..848d9d8a98 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -391,6 +391,8 @@ final class PeerInfoScreenData { let profileGiftsCollectionsContext: ProfileGiftsCollectionsContext? let premiumGiftOptions: [PremiumGiftCodeOption] let webAppPermissions: WebAppPermissionsState? + let savedMusicContext: ProfileSavedMusicContext? + let savedMusicState: ProfileSavedMusicContext.State? let _isContact: Bool var forceIsContact: Bool = false @@ -443,7 +445,9 @@ final class PeerInfoScreenData { profileGiftsContext: ProfileGiftsContext?, profileGiftsCollectionsContext: ProfileGiftsCollectionsContext?, premiumGiftOptions: [PremiumGiftCodeOption], - webAppPermissions: WebAppPermissionsState? + webAppPermissions: WebAppPermissionsState?, + savedMusicContext: ProfileSavedMusicContext?, + savedMusicState: ProfileSavedMusicContext.State? ) { self.peer = peer self.chatPeer = chatPeer @@ -485,6 +489,8 @@ final class PeerInfoScreenData { self.profileGiftsCollectionsContext = profileGiftsCollectionsContext self.premiumGiftOptions = premiumGiftOptions self.webAppPermissions = webAppPermissions + self.savedMusicContext = savedMusicContext + self.savedMusicState = savedMusicState } } @@ -1005,12 +1011,30 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, profileGiftsContext: profileGiftsContext, profileGiftsCollectionsContext: nil, premiumGiftOptions: [], - webAppPermissions: nil + webAppPermissions: nil, + savedMusicContext: nil, + savedMusicState: nil ) } } -func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?, existingProfileGiftsContext: ProfileGiftsContext?, existingProfileGiftsCollectionsContext: ProfileGiftsCollectionsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, privacySettings: Signal, forceHasGifts: Bool) -> Signal { +func peerInfoScreenData( + context: AccountContext, + peerId: PeerId, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + isSettings: Bool, + isMyProfile: Bool, + hintGroupInCommon: PeerId?, + existingRequestsContext: PeerInvitationImportersContext?, + existingProfileGiftsContext: ProfileGiftsContext?, + existingProfileGiftsCollectionsContext: ProfileGiftsCollectionsContext?, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic, + sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, + privacySettings: Signal, + forceHasGifts: Bool +) -> Signal { return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings) |> mapToSignal { inputData -> Signal in let wasUpgradedGroup = Atomic(value: nil) @@ -1057,7 +1081,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen profileGiftsContext: nil, profileGiftsCollectionsContext: nil, premiumGiftOptions: [], - webAppPermissions: nil + webAppPermissions: nil, + savedMusicContext: nil, + savedMusicState: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1377,6 +1403,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return .single(nil) } } + + let savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: peerId) return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), @@ -1398,9 +1426,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, - webAppPermissions + webAppPermissions, + savedMusicContext.state ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions, savedMusicState -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1536,7 +1565,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen profileGiftsContext: profileGiftsContext, profileGiftsCollectionsContext: profileGiftsCollectionsContext, premiumGiftOptions: premiumGiftOptions, - webAppPermissions: webAppPermissions + webAppPermissions: webAppPermissions, + savedMusicContext: savedMusicContext, + savedMusicState: savedMusicState ) } case .channel: @@ -1779,7 +1810,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen profileGiftsContext: profileGiftsContext, profileGiftsCollectionsContext: profileGiftsCollectionsContext, premiumGiftOptions: [], - webAppPermissions: nil + webAppPermissions: nil, + savedMusicContext: nil, + savedMusicState: nil ) } case let .group(groupId): @@ -2113,7 +2146,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen profileGiftsContext: nil, profileGiftsCollectionsContext: nil, premiumGiftOptions: [], - webAppPermissions: nil + webAppPermissions: nil, + savedMusicContext: nil, + savedMusicState: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 989fe68571..52f8be80f5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -41,6 +41,9 @@ import MultilineTextComponent import PeerInfoRatingComponent import UndoUI import ProfileLevelInfoScreen +import PlainButtonComponent +import BundleIconComponent +import MarqueeComponent final class PeerInfoHeaderNavigationTransition { let sourceNavigationBar: NavigationBar @@ -160,6 +163,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { let editingNavigationBackgroundNode: NavigationBackgroundNode let editingNavigationBackgroundSeparator: ASDisplayNode + var musicBackground: UIView? + var music: ComponentView? + var performButtonAction: ((PeerInfoHeaderButtonKey, ContextGesture?) -> Void)? var requestAvatarExpansion: ((Bool, [AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)? var requestOpenAvatarForEditing: ((Bool) -> Void)? @@ -171,6 +177,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)? var displayEmojiPackTooltip: (() -> Void)? + var displaySavedMusic: (() -> Void)? + var displayPremiumIntro: ((UIView, PeerEmojiStatus?, Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError>, Bool) -> Void)? var displayStatusPremiumIntro: (() -> Void)? var displayUniqueGiftInfo: ((UIView, String) -> Void)? @@ -307,7 +315,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.animationRenderer = context.animationRenderer super.init() - + requestUpdateLayoutImpl = { [weak self] in self?.requestUpdateLayout?(false) } @@ -501,7 +509,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentStatusIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, profileGiftsContext: ProfileGiftsContext?, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, profileGiftsContext: ProfileGiftsContext?, screenData: PeerInfoScreenData?, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { if self.appliedCustomNavigationContentNode !== self.customNavigationContentNode { if let previous = self.appliedCustomNavigationContentNode { transition.updateAlpha(node: previous, alpha: 0.0, completion: { [weak previous] _ in @@ -549,6 +557,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + + var currentSavedMusic: TelegramMediaFile? + if !self.isSettings, let screenData { + if let savedMusicState = screenData.savedMusicState { + currentSavedMusic = savedMusicState.files.first + } else if let cachedUserData = screenData.cachedData as? CachedUserData { + currentSavedMusic = cachedUserData.savedMusic + } + } + let musicHeight: CGFloat = 24.0 + let bottomInset: CGFloat = currentSavedMusic != nil ? 24.0 : 0.0 + let isLandscape = containerInset > 16.0 let themeUpdated = self.presentationData?.theme !== presentationData.theme @@ -620,11 +640,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { let regularContentButtonBackgroundColor: UIColor let collapsedHeaderContentButtonBackgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - let expandedAvatarContentButtonBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.1) + let expandedAvatarContentButtonBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 0.1) let regularHeaderButtonBackgroundColor: UIColor let collapsedHeaderButtonBackgroundColor: UIColor = .clear - let expandedAvatarHeaderButtonBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.1) + let expandedAvatarHeaderButtonBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 0.1) let regularContentButtonForegroundColor: UIColor = peer?.profileColor != nil ? UIColor.white : presentationData.theme.list.itemAccentColor let collapsedHeaderContentButtonForegroundColor = presentationData.theme.list.itemAccentColor @@ -1590,7 +1610,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isAvatarExpanded { let minTitleSize = CGSize(width: titleSize.width * expandedTitleScale, height: titleSize.height * expandedTitleScale) - var minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) + var minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - bottomInset - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) if !self.isSettings && !self.isMyProfile { minTitleFrame.origin.y -= 83.0 } @@ -1963,6 +1983,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { expandablePart += 99.0 } } + expandablePart += bottomInset height = navigationHeight + max(0.0, expandablePart) maxY = navigationHeight + panelWithAvatarHeight - contentOffset backgroundHeight = height @@ -2271,12 +2292,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing let buttonSize = CGSize(width: buttonWidth, height: 58.0) - var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: backgroundHeight - 16.0 - buttonSize.height) + var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: backgroundHeight - bottomInset - 16.0 - buttonSize.height) if !actionButtonKeys.isEmpty { buttonRightOrigin.y += actionButtonSize.height + 24.0 } - transition.updateFrameAdditive(node: self.buttonsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonRightOrigin.y), size: CGSize(width: width, height: buttonSize.height))) + transition.updateFrameAdditive(node: self.buttonsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonRightOrigin.y), size: CGSize(width: width, height: buttonSize.height + 40.0))) self.buttonsBackgroundNode.update(size: self.buttonsBackgroundNode.bounds.size, transition: transition) self.buttonsBackgroundNode.updateColor(color: contentButtonBackgroundColor, enableBlur: true, transition: transition) @@ -2398,7 +2419,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isAvatarExpanded { resolvedRegularHeight = expandedAvatarListSize.height } else { - resolvedRegularHeight = panelWithAvatarHeight + navigationHeight + resolvedRegularHeight = panelWithAvatarHeight + navigationHeight + bottomInset } let backgroundFrame: CGRect @@ -2579,6 +2600,129 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + if let currentSavedMusic { + var musicTransition = transition + var artist = presentationData.strings.MediaPlayer_UnknownArtist + var track: String? + for attribute in currentSavedMusic.attributes { + if case let .Audio(_, _, title, performer, _) = attribute { + artist = performer ?? artist + track = title + break + } + } + if track == nil { + if let fileName = currentSavedMusic.fileName { + track = fileName + } else { + track = presentationData.strings.MediaPlayer_UnknownTrack + } + } + + if hasBackground || self.isAvatarExpanded { + if self.musicBackground == nil { + musicTransition = .immediate + } + let musicBackground = self.musicBackground ?? { + let musicBackground = UIView() + musicBackground.backgroundColor = .white + self.buttonsMaskView.addSubview(musicBackground) + self.musicBackground = musicBackground + if transition.isAnimated { + musicBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + return musicBackground + }() + musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: musicHeight))) + } else if let musicBackground = self.musicBackground { + self.musicBackground = nil + if transition.isAnimated { + musicBackground.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + musicBackground.removeFromSuperview() + }) + } else { + musicBackground.removeFromSuperview() + } + } + + let music = self.music ?? { + let componentView = ComponentView() + self.music = componentView + return componentView + }() + + let musicString = NSMutableAttributedString() + let isOverlay = self.isAvatarExpanded || hasBackground + musicString.append(NSAttributedString(string: track ?? "", font: Font.semibold(12.0), textColor: isOverlay ? .white : presentationData.theme.list.itemAccentColor)) + musicString.append(NSAttributedString(string: " - \(artist)", font: Font.regular(12.0), textColor: isOverlay ? UIColor.white.withAlphaComponent(0.7) : presentationData.theme.list.itemSecondaryTextColor)) + + let musicSize = music.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Media Editor/SmallAudio", tintColor: isOverlay ? .white : presentationData.theme.list.itemAccentColor)) + ), + AnyComponentWithIdentity( + id: "label", + component: AnyComponent(MarqueeComponent(attributedText: musicString, maxWidth: backgroundFrame.width - 96.0)) + ), + AnyComponentWithIdentity( + id: "arrow", + component: AnyComponent(BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: isOverlay ? .white : presentationData.theme.list.itemSecondaryTextColor)) + ) + ], spacing: 4.0) + ), + minSize: CGSize(width: backgroundFrame.width, height: musicHeight), + action: { [weak self] in + self?.displaySavedMusic?() + } + ) + ), + environment: {}, + containerSize: CGSize(width: backgroundFrame.width, height: musicHeight) + ) + let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight), size: musicSize) + if let musicView = music.view { + if musicView.superview == nil { + self.view.addSubview(musicView) + if transition.isAnimated { + musicView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + if additive { + musicTransition.updateFrameAdditiveToCenter(view: musicView, frame: musicFrame) + } else { + musicTransition.updateFrame(view: musicView, frame: musicFrame) + } + musicTransition.updateAlpha(layer: musicView.layer, alpha: backgroundBannerAlpha) + } + } else { + if let musicBackground = self.musicBackground { + self.musicBackground = nil + if transition.isAnimated { + musicBackground.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + musicBackground.removeFromSuperview() + }) + } else { + musicBackground.removeFromSuperview() + } + } + if let music = self.music { + self.music = nil + if transition.isAnimated { + music.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + music.view?.removeFromSuperview() + }) + } else { + music.view?.removeFromSuperview() + } + } + } + if isFirstTime { self.updateAvatarMask(transition: .immediate) } @@ -2594,6 +2738,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.performButtonAction?(buttonNode.key, gesture) } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + var result = super.point(inside: point, with: event) + if let musicView = self.music?.view, musicView.frame.contains(point) { + result = true + } + return result + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index df4b42309b..68ecabb5b0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4114,6 +4114,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.performButtonAction(key: key, gesture: gesture) } + self.headerNode.displaySavedMusic = { [weak self] in + self?.displaySavedMusic() + } + self.headerNode.cancelUpload = { [weak self] in guard let strongSelf = self else { return @@ -6004,6 +6008,68 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func displaySavedMusic() { + guard let savedMusicContext = self.data?.savedMusicContext else { + return + } + let peerId = self.peerId + let peer = self.data?.peer + let initialMessageId: MessageId + if let initialFileId = self.data?.savedMusicState?.files.first?.fileId { + initialMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(clamping: initialFileId.id % Int64(Int32.max))) + } else { + initialMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0) + } + + let musicController = self.context.sharedContext.makeOverlayAudioPlayerController( + context: self.context, + chatLocation: .peer(id: peerId), + type: .music, + initialMessageId: initialMessageId, + initialOrder: .regular, + playlistLocation: PeerMessagesPlaylistLocation.custom( + messages: savedMusicContext.state + |> map { state in + var messages: [Message] = [] + var peers = SimpleDictionary() + peers[peerId] = peer + for file in state.files { + let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) + messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) + + } + return (messages, Int32(messages.count), true) + }, + canReorder: peerId == self.context.account.peerId, + at: initialMessageId, + loadMore: { [weak savedMusicContext] in + guard let savedMusicContext else { + return + } + savedMusicContext.loadMore() + } + ), + parentNavigationController: self.controller?.navigationController as? NavigationController, + updateMusicSaved: { [weak savedMusicContext] file, isSaved in + guard let savedMusicContext else { + return + } + if isSaved { + let _ = savedMusicContext.addMusic(file: file).start() + } else { + let _ = savedMusicContext.removeMusic(file: file).start() + } + }, + reorderSavedMusic: { [weak savedMusicContext] file, afterFile in + guard let savedMusicContext else { + return + } + let _ = savedMusicContext.addMusic(file: file, afterFile: afterFile, apply: true).start() + } + ) + self.controller?.present(musicController, in: .window(.root)) + } + private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) { guard let controller = self.controller else { return @@ -12245,7 +12311,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -12630,7 +12696,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -14320,7 +14386,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, profileGiftsContext: self.screenNode.data?.profileGiftsContext, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, profileGiftsContext: self.screenNode.data?.profileGiftsContext, screenData: self.screenNode.data, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index f6bfb5ca17..5725b96903 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -632,7 +632,7 @@ public final class TabSelectorComponent: Component { } let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * (spacing + innerInset)) - if component.customLayout?.fillWidth == true && estimatedContentWidth < availableSize.width { + if component.customLayout?.fillWidth == true && estimatedContentWidth < availableSize.width && component.items.count > 1 { spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) - innerInset * 2.0 } else if estimatedContentWidth > availableSize.width && !allowScroll { spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/Contents.json new file mode 100644 index 0000000000..8b91afdd9f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "profilemusic_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/profilemusic_20.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/profilemusic_20.pdf new file mode 100644 index 0000000000..35a49f76c7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/profilemusic_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/Contents.json new file mode 100644 index 0000000000..8c3342fe88 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "profilemusic_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/profilemusic_30.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/profilemusic_30.pdf new file mode 100644 index 0000000000..286ad31c81 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/profilemusic_30.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_gift.tgs b/submodules/TelegramUI/Resources/Animations/anim_gift.tgs new file mode 100644 index 0000000000..1a0c0d6cf8 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_gift.tgs differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7604ca828e..afbf2d5762 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4430,29 +4430,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } var file: TelegramMediaFile? - var title: String? - var performer: String? for media in message.media { if let mediaFile = media as? TelegramMediaFile, mediaFile.isMusic { file = mediaFile - for attribute in mediaFile.attributes { - if case let .Audio(_, _, titleValue, performerValue, _) = attribute { - if let titleValue, !titleValue.isEmpty { - title = titleValue - } - if let performerValue, !performerValue.isEmpty { - performer = performerValue - } - } - } } } guard let file else { return } - var signal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .message(message: MessageReference(message._asMessage()), media: file)) - let disposable: MetaDisposable if let current = self.saveMediaDisposable { disposable = current @@ -4460,108 +4446,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable = MetaDisposable() self.saveMediaDisposable = disposable } - - var cancelImpl: (() -> Void)? - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak self] subscriber in - guard let self else { - return EmptyDisposable - } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() + disposable.set( + saveMediaToFiles( + context: self.context, + fileReference: .message(message: MessageReference(message._asMessage()), media: file), + present: { [weak self] c, a in + guard let self else { + return + } + self.present(c, in: .window(.root), with: a) } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.startStrict() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { [weak disposable] in - disposable?.set(nil) - } - disposable.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] state, _ in - guard let self else { - return - } - switch state { - case .progress: - break - case let .data(data): - if data.complete { - var symlinkPath = data.path + ".mp3" - if fileSize(symlinkPath) != nil { - try? FileManager.default.removeItem(atPath: symlinkPath) - } - let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath) - - let audioUrl = URL(fileURLWithPath: symlinkPath) - let audioAsset = AVURLAsset(url: audioUrl) - - var fileExtension = "mp3" - if let filename = file.fileName { - if let dotIndex = filename.lastIndex(of: ".") { - fileExtension = String(filename[filename.index(after: dotIndex)...]) - } - } - - var nameComponents: [String] = [] - if let title { - if let performer { - nameComponents.append(performer) - } - nameComponents.append(title) - } else { - var artist: String? - var title: String? - for data in audioAsset.commonMetadata { - if data.commonKey == .commonKeyArtist { - artist = data.stringValue - } - if data.commonKey == .commonKeyTitle { - title = data.stringValue - } - } - if let artist, !artist.isEmpty { - nameComponents.append(artist) - } - if let title, !title.isEmpty { - nameComponents.append(title) - } - if nameComponents.isEmpty, var filename = file.fileName { - if let dotIndex = filename.lastIndex(of: ".") { - filename = String(filename[.. map { messages, accountPeer -> ([Message], Int32, Bool) in @@ -567,7 +567,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return (messages, Int32(messages.count), false) } - source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote.flatMap { quote in ChatHistoryListSource.Quote(text: quote.text, offset: quote.offset) }, loadMore: nil) + source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote.flatMap { quote in ChatHistoryListSource.Quote(text: quote.text, offset: quote.offset) }, updateAll: true, canReorder: false, loadMore: nil) case let .link(link): let messages = link.options |> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in @@ -668,13 +668,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return ([message], 1, false) } - source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil) + source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, updateAll: true, canReorder: false, loadMore: nil) } } else if case .customChatContents = chatLocation { if case let .customChatContents(customChatContents) = subject { source = .customView(historyView: customChatContents.historyView) } else { - source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, loadMore: nil) + source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, updateAll: true, canReorder: false, loadMore: nil) } } else { source = .default diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 7807ee0c7a..353975c903 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -216,7 +216,7 @@ extension ListMessageItemInteraction { } } -private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -239,7 +239,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) + item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, canReorder: canReorder) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -275,7 +275,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -298,7 +298,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) + item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, canReorder: canReorder) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -334,11 +334,11 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) +private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) } -private final class ChatHistoryTransactionOpaqueState { +final class ChatHistoryTransactionOpaqueState { let historyView: ChatHistoryView init(historyView: ChatHistoryView) { @@ -1398,8 +1398,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?, Set), NoError> var isFirstTime = true var updateAllOnEachVersion = false - if case let .custom(messages, at, quote, _) = self.source { - updateAllOnEachVersion = true + var canReorder = false + if case let .custom(messages, at, quote, updateAll, canReorderValue, _) = self.source { + updateAllOnEachVersion = updateAll + canReorder = canReorderValue historyViewUpdate = messages |> map { messages, _, hasMore in let version = currentViewVersion.modify({ value in @@ -1916,7 +1918,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let forceSynchronous = true let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: nil, scrollAnimationCurve: nil, initialData: initialData?.initialData, keyboardButtonsMessage: nil, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: false, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: false) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) @@ -2307,7 +2309,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 9fec1c3b27..f08548e652 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -381,6 +381,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 3 + self.textNode.truncationType = .middle self.textNode.textAlignment = .center super.init() @@ -556,7 +557,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { transition.updateAlpha(node: self.textNode, alpha: 1.0) - let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 80.0, height: 40.0)) + let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 80.0, height: 80.0)) self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: 10.0), size: textSize) for (_, view) in self.buttons { @@ -565,7 +566,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.tapGestureRecognizer?.isEnabled = true - panelHeight += 15.0 + panelHeight += max(15.0, textSize.height - 19.0) } else { transition.updateAlpha(node: self.textNode, alpha: 0.0) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index e96f9c4833..fd9589a119 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -17,7 +17,9 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer let initialOrder: MusicPlaybackSettingsOrder let playlistLocation: SharedMediaPlaylistLocation? - private weak var parentNavigationController: NavigationController? + private(set) weak var parentNavigationController: NavigationController? + private let updateMusicSaved: ((FileMediaReference, Bool) -> Void)? + let reorderSavedMusic: ((FileMediaReference, FileMediaReference?) -> Void)? private var animatedIn = false @@ -27,7 +29,17 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer private var accountInUseDisposable: Disposable? - init(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation? = nil, parentNavigationController: NavigationController?) { + init( + context: AccountContext, + chatLocation: ChatLocation, + type: MediaManagerPlayerType, + initialMessageId: MessageId, + initialOrder: MusicPlaybackSettingsOrder, + playlistLocation: SharedMediaPlaylistLocation? = nil, + parentNavigationController: NavigationController?, + updateMusicSaved: ((FileMediaReference, Bool) -> Void)? = nil, + reorderSavedMusic: ((FileMediaReference, FileMediaReference?) -> Void)? = nil + ) { self.context = context self.chatLocation = chatLocation self.type = type @@ -35,6 +47,8 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer self.initialOrder = initialOrder self.playlistLocation = playlistLocation self.parentNavigationController = parentNavigationController + self.updateMusicSaved = updateMusicSaved + self.reorderSavedMusic = reorderSavedMusic super.init(navigationBarPresentationData: nil) @@ -111,7 +125,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer guard let self, let peer else { return } - guard let navigationController = self.navigationController as? NavigationController else { + guard let navigationController = self.parentNavigationController else { return } self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) @@ -133,10 +147,22 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer strongSelf.context.sharedContext.openSearch(filter: .music, query: artist) strongSelf.dismiss() } - }) - self.controllerNode.getParentController = { [weak self] in + }, updateMusicSaved: { [weak self] file, isSaved in + if let self { + if let updateMusicSaved = self.updateMusicSaved { + updateMusicSaved(file, isSaved) + } else { + if isSaved { + let _ = self.context.engine.peers.addSavedMusic(file: file).start() + } else { + let _ = self.context.engine.peers.removeSavedMusic(file: file).start() + } + } + } + }, + getParentController: { [weak self] in return self - } + }) self.ready.set(self.controllerNode.ready.get()) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2456775353..acefc1a9fe 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -11,18 +11,23 @@ import AccountContext import DirectionalPanGesture import ChatPresentationInterfaceState import ChatControllerInteraction +import ContextUI +import UndoUI +import ChatHistoryEntry final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestureRecognizerDelegate { let ready = Promise() private let context: AccountContext + private let source: ChatHistoryListSource private let chatLocation: ChatLocation private var presentationData: PresentationData private let type: MediaManagerPlayerType private let requestDismiss: () -> Void private let requestShare: (MessageId) -> Void private let requestSearchByArtist: (String) -> Void + private let updateMusicSaved: (FileMediaReference, Bool) -> Void private let playlistLocation: SharedMediaPlaylistLocation? private let isGlobalSearch: Bool @@ -40,18 +45,32 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu private var replacementHistoryNode: ChatHistoryListNodeImpl? private var replacementHistoryNodeFloatingOffset: CGFloat? + private var saveMediaDisposable: MetaDisposable? + private var validLayout: ContainerViewLayout? private var presentationDataDisposable: Disposable? private let replacementHistoryNodeReadyDisposable = MetaDisposable() - var getParentController: () -> ViewController? = { return nil } { - didSet { - self.controlsNode.getParentController = self.getParentController - } - } + private let getParentController: () -> ViewController? + + private var savedIdsDisposable: Disposable? + private var savedIdsPromise = Promise?>() + private var savedIds: Set? - init(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, requestDismiss: @escaping () -> Void, requestShare: @escaping (MessageId) -> Void, requestSearchByArtist: @escaping (String) -> Void) { + init( + context: AccountContext, + chatLocation: ChatLocation, + type: MediaManagerPlayerType, + initialMessageId: MessageId, + initialOrder: MusicPlaybackSettingsOrder, + playlistLocation: SharedMediaPlaylistLocation?, + requestDismiss: @escaping () -> Void, + requestShare: @escaping (MessageId) -> Void, + requestSearchByArtist: @escaping (String) -> Void, + updateMusicSaved: @escaping (FileMediaReference, Bool) -> Void, + getParentController: @escaping () -> ViewController? + ) { self.context = context self.chatLocation = chatLocation self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -60,6 +79,16 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.requestShare = requestShare self.requestSearchByArtist = requestSearchByArtist self.playlistLocation = playlistLocation + self.updateMusicSaved = updateMusicSaved + self.getParentController = getParentController + + if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, at, loadMore) = playlistLocation { + self.source = .custom(messages: messages, messageId: at, quote: nil, updateAll: false, canReorder: canReorder, loadMore: loadMore) + self.isGlobalSearch = false + } else { + self.source = .default + self.isGlobalSearch = false + } if case .regular = initialOrder { self.currentIsReversed = false @@ -68,6 +97,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } var openMessageImpl: ((MessageId) -> Bool)? + var openMessageContextMenuImpl: ((Message, ASDisplayNode, CGRect, Any?) -> Void)? self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in if let openMessageImpl = openMessageImpl { return openMessageImpl(message.id) @@ -76,7 +106,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in - }, openMessageContextMenu: { _, _, _, _, _, _ in + }, openMessageContextMenu: { message, _, node, rect, gesture, _ in + openMessageContextMenuImpl?(message, node, rect, gesture) }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in @@ -205,7 +236,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.contentNode = ASDisplayNode() - self.controlsNode = OverlayPlayerControlsNode(account: context.account, engine: context.engine, accountManager: context.sharedContext.accountManager, presentationData: self.presentationData, status: context.sharedContext.mediaManager.musicMediaPlayerState) + self.controlsNode = OverlayPlayerControlsNode(account: context.account, engine: context.engine, accountManager: context.sharedContext.accountManager, presentationData: self.presentationData, status: context.sharedContext.mediaManager.musicMediaPlayerState, chatLocation: self.chatLocation, source: self.source) + self.controlsNode.getParentController = getParentController self.historyBackgroundNode = ASDisplayNode() self.historyBackgroundNode.isLayerBacked = true @@ -228,17 +260,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let chatLocationContextHolder = Atomic(value: nil) - let source: ChatHistoryListSource - if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, at, loadMore) = playlistLocation { - source = .custom(messages: messages, messageId: at, quote: nil, loadMore: loadMore) - self.isGlobalSearch = true - } else { - source = .default - self.isGlobalSearch = false - } - - self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) + self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: self.source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) self.historyNode.clipsToBounds = true + //self.historyNode.areContentAnimationsEnabled = true super.init() @@ -269,7 +293,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.controlsNode.updateIsExpanded = { [weak self] in if let strongSelf = self, let validLayout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring)) + strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.5, curve: .spring)) } } @@ -285,6 +309,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self?.requestSearchByArtist(artist) } + self.controlsNode.requestLayout = { [weak self] transition in + if let self, let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) + } + } + self.controlsNode.updateOrder = { [weak self] order in if let strongSelf = self { let reversed: Bool @@ -308,6 +338,18 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } } + self.controlsNode.requestSaveToProfile = { [weak self] file in + if let self { + self.addToSavedMusic(file: file) + } + } + + self.controlsNode.requestRemoveFromProfile = { [weak self] file in + if let self { + self.removeFromSavedMusic(file: file) + } + } + self.addSubnode(self.dimNode) self.addSubnode(self.contentNode) self.contentNode.addSubnode(self.historyBackgroundNode) @@ -321,8 +363,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu openMessageImpl = { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) { var playlistLocation: PeerMessagesPlaylistLocation? - if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, _, loadMore) = location { - playlistLocation = .custom(messages: messages, at: id, loadMore: loadMore) + if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, _, loadMore) = location { + playlistLocation = .custom(messages: messages, canReorder: canReorder, at: id, loadMore: loadMore) } return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation)) @@ -330,6 +372,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu return false } + openMessageContextMenuImpl = { [weak self] message, node, rect, gesture in + guard let self else { + return + } + self.openMessageContextMenu(message: message, node: node, frame: rect, gesture: gesture as? ContextGesture) + } + self.presentationDataDisposable = context.sharedContext.presentationData.startStrict(next: { [weak self] presentationData in if let strongSelf = self { if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings { @@ -338,14 +387,45 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } }) - self.ready.set(self.historyNode.historyState.get() |> map { _ -> Bool in - return true - } |> take(1)) + self.savedIdsDisposable = (context.engine.peers.savedMusicIds() + |> deliverOnMainQueue).start(next: { [weak self] savedIds in + guard let self else { + return + } + let isFirstTime = self.savedIds == nil + self.savedIds = savedIds + self.savedIdsPromise.set(.single(savedIds)) + + let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring) + self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition) + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) + } + }) + + self.ready.set( + combineLatest( + self.historyNode.historyState.get() + |> take(1), + self.savedIdsPromise.get() + |> filter { + $0 != nil + } + |> take(1) + ) + |> map { _, _ -> Bool in + return true + } + ) + + self.setupReordering() } deinit { self.presentationDataDisposable?.dispose() self.replacementHistoryNodeReadyDisposable.dispose() + self.savedIdsDisposable?.dispose() + self.saveMediaDisposable?.dispose() } override func didLoad() { @@ -371,6 +451,49 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.view.addGestureRecognizer(panRecognizer) } + private func setupReordering() { + guard let controller = self.getParentController() as? OverlayAudioPlayerControllerImpl, let reorderSavedMusic = controller.reorderSavedMusic, case let .peer(peerId) = self.chatLocation else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer = peer.flatMap({ PeerReference($0._asPeer()) }) else { + return + } + self.historyNode.reorderItem = { fromIndex, toIndex, transactionOpaqueState -> Signal in + guard let filteredEntries = (transactionOpaqueState as? ChatHistoryTransactionOpaqueState)?.historyView.filteredEntries else { + return .single(false) + } + let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] + let toEntry: ChatHistoryEntry? + if toIndex == 0 { + toEntry = nil + } else { + toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] + } + guard case let .MessageEntry(fromMessage, _, _, _, _, _) = fromEntry else { + return .single(false) + } + + guard let fromFile = fromMessage.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile else { + return .single(false) + } + + var toFile: TelegramMediaFile? + if let toEntry, case let .MessageEntry(toMessage, _, _, _, _, _) = toEntry, let file = toMessage.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { + toFile = file + } + if fromFile.id == toFile?.id { + return .single(false) + } + + reorderSavedMusic(.savedMusic(peer: peer, media: fromFile), toFile.flatMap { .savedMusic(peer: peer, media: $0) }) + + return .single(true) + } + }) + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData @@ -378,6 +501,128 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.controlsNode.updatePresentationData(self.presentationData) } + private func dismissAllTooltips() { + guard let controller = self.getParentController() else { + return + } + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + } + + func forwardToSavedMessages(file: FileMediaReference) { + self.dismissAllTooltips() + + let _ = self.context.engine.messages.enqueueOutgoingMessage(to: self.context.account.peerId, replyTo: nil, content: .file(file)).start() + + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .forward(savedMessages: true, text: "Audio forwarded to Saved Messages."), + action: { _ in + return true + } + ) + self.getParentController()?.present(controller, in: .window(.root)) + } + + func addToSavedMusic(file: FileMediaReference) { + self.dismissAllTooltips() + + var actionText: String? = "View" + if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId { + actionText = nil + } + + //TODO:localize + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, + size: nil, + title: nil, + text: "Audio added to your profile.", + customUndoText: actionText, + timeout: 3.0 + ), + action: { [weak self] action in + if let self, case .undo = action { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + if let controller = self.context.sharedContext.makePeerInfoController( + context: self.context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + if let navigationController = (self.getParentController() as? OverlayAudioPlayerControllerImpl)?.parentNavigationController { + self.requestDismiss() + navigationController.pushViewController(controller) + } + } + }) + } + return true + } + ) + self.getParentController()?.present(controller, in: .window(.root)) + + self.updateMusicSaved(file, true) + } + + func removeFromSavedMusic(file: FileMediaReference) { + self.dismissAllTooltips() + + //TODO:localize + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, + size: nil, + title: nil, + text: "Audio removed from your profile.", + customUndoText: nil, + timeout: 3.0 + ), + action: { _ in + return true + } + ) + + if self.historyNode.originalHistoryView?.entries.count == 1 { + if let navigationController = (self.getParentController() as? OverlayAudioPlayerControllerImpl)?.parentNavigationController { + self.requestDismiss() + navigationController.presentOverlay(controller: controller) + + self.context.sharedContext.mediaManager.setPlaylist(nil, type: self.type, control: .playback(.pause)) + } + } else { + self.getParentController()?.present(controller, in: .window(.root)) + } + + self.updateMusicSaved(file, false) + } + + private var isSaved: Bool? { + guard let fileReference = self.controlsNode.currentFileReference else { + return nil + } + return self.savedIds?.contains(fileReference.media.fileId.id) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout @@ -397,7 +642,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) - let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved) let listTopInset = layoutTopInset + controlsHeight @@ -423,6 +668,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } func animateOut(completion: (() -> Void)?) { + self.dismissAllTooltips() + self.layer.animateBoundsOriginYAdditive(from: self.bounds.origin.y, to: -self.bounds.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion?() }) @@ -479,7 +726,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: - break + self.dismissAllTooltips() case .changed: let translation = recognizer.translation(in: self.contentNode.view) var bounds = self.contentNode.bounds @@ -533,7 +780,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5) - let controlsHeight = self.controlsNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, transition: transition) + let controlsHeight = self.controlsNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, hasSectionHeader: true, savedMusic: self.isSaved, transition: transition) let listTopInset = layoutTopInset + controlsHeight @@ -580,7 +827,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } let chatLocationContextHolder = Atomic(value: nil) - let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: .default, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) + let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: self.source, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) historyNode.clipsToBounds = true historyNode.preloadPages = true historyNode.stackFromBottom = true @@ -598,7 +845,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) - let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved) let listTopInset = layoutTopInset + controlsHeight @@ -626,6 +873,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu previousHistoryNode.disconnect() self.contentNode.insertSubnode(replacementHistoryNode, belowSubnode: self.historyNode) self.historyNode = replacementHistoryNode + self.setupReordering() if let validLayout = self.validLayout, let offset = self.replacementHistoryNodeFloatingOffset, let previousOffset = self.floatingHeaderOffset { let offsetDelta = offset - previousOffset @@ -634,7 +882,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5) - let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved) let listTopInset = layoutTopInset + controlsHeight @@ -695,7 +943,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5) - let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded) + let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved) let listTopInset = layoutTopInset + controlsHeight @@ -712,4 +960,261 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } } } + + private func openMessageContextMenu(message: Message, node: ASDisplayNode, frame: CGRect, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, location: CGPoint? = nil) { + guard let node = node as? ContextExtractedContentContainingNode, let peer = message.peers[message.id.peerId].flatMap({ PeerReference($0) }), let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile else { + return + } + let context = self.context + let presentationData = self.presentationData + let source: ContextContentSource = .extracted(OverlayAudioPlayerContextExtractedContentSource(contentNode: node)) + let fileReference: FileMediaReference = message.id.namespace == Namespaces.Message.Local ? .savedMusic(peer: peer, media: file) : .message(message: MessageReference(message), media: file) + + let canSaveToProfile = !(self.savedIds?.contains(file.fileId.id) == true) + let canSaveToSavedMessages = message.id.peerId != self.context.account.peerId + + let _ = (context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: [message.id], keepUpdated: false) + |> deliverOnMainQueue).startStandalone(next: { [weak self] actions in + guard let self else { + return + } + + var items: [ContextMenuItem] = [] + //TODO:localize + if canSaveToProfile || canSaveToSavedMessages { + items.append( + .action(ContextMenuActionItem(text: "Save to...", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + if let self { + var subActions: [ContextMenuItem] = [] + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c, _ in + c?.popItems() + })) + ) + subActions.append(.separator) + + if canSaveToProfile { + subActions.append( + .action(ContextMenuActionItem(text: "…Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let self { + self.addToSavedMusic(file: fileReference) + } + })) + ) + } + + if canSaveToSavedMessages { + subActions.append( + .action(ContextMenuActionItem(text: "…Saved Messages", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let self { + self.forwardToSavedMessages(file: fileReference) + } + })) + ) + } + + subActions.append( + .action(ContextMenuActionItem(text: "…Files", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let self { + let disposable: MetaDisposable + if let current = self.saveMediaDisposable { + disposable = current + } else { + disposable = MetaDisposable() + self.saveMediaDisposable = disposable + } + disposable.set( + saveMediaToFiles(context: context, fileReference: fileReference, present: { [weak self] c, a in + if let self, let controller = (self.getParentController() as? OverlayAudioPlayerControllerImpl) { + controller.present(c, in: .window(.root), with: a) + } + }) + ) + } + })) + ) + + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + subActions.append( + .action(ContextMenuActionItem(text: "Choose where you want this audio to be saved.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) + ) + + c?.pushItems(items: .single(ContextController.Items(content: .list(subActions)))) + } + })) + ) + } else { + items.append(.action(ContextMenuActionItem(text: "Save to Files", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + if let self { + let disposable: MetaDisposable + if let current = self.saveMediaDisposable { + disposable = current + } else { + disposable = MetaDisposable() + self.saveMediaDisposable = disposable + } + disposable.set( + saveMediaToFiles(context: context, fileReference: fileReference, present: { [weak self] c, a in + if let self, let controller = (self.getParentController() as? OverlayAudioPlayerControllerImpl) { + controller.present(c, in: .window(.root), with: a) + } + }) + ) + } + }))) + } + + items.append(.separator) + + if message.id.namespace == Namespaces.Message.Cloud { + items.append( + .action(ContextMenuActionItem(text: "Show in Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self else { + return + } + context.sharedContext.navigateToChat(accountId: context.account.id, peerId: message.id.peerId, messageId: message.id) + self.requestDismiss() + })) + ) + } + + // items.append( + // .action(ContextMenuActionItem(text: "Forward", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + // f(.default) + // + // if let _ = self { + // + // } + // })) + // ) + + var canDelete = false + if message.id.namespace == Namespaces.Message.Local { + canDelete = true + } else if let peer = message.peers[message.id.peerId] { + if peer is TelegramUser || peer is TelegramSecretChat { + canDelete = true + } else if let _ = peer as? TelegramGroup { + canDelete = true + } else if let channel = peer as? TelegramChannel { + if message.flags.contains(.Incoming) { + canDelete = channel.hasPermission(.deleteAllMessages) + } else { + canDelete = true + } + } else { + canDelete = false + } + } else { + canDelete = false + } + + if canDelete { + var actionTitle = "Delete" + if case .custom = self.source { + actionTitle = "Remove" + } + items.append( + .action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in + guard let self else { + return + } + if message.id.namespace == Namespaces.Message.Local { + f(.default) + self.removeFromSavedMusic(file: fileReference) + } else { + c?.setItems(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> map { peer -> ContextController.Items in + var items: [ContextMenuItem] = [] + let messageIds = [message.id] + + if let peer { + var personalPeerName: String? + var isChannel = false + if case let .user(user) = peer { + personalPeerName = EnginePeer(user).compactDisplayTitle + } else if case let .channel(channel) = peer, case .broadcast = channel.info { + isChannel = true + } + + if actions.options.contains(.deleteGlobally) { + let globalTitle: String + if isChannel { + globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone + } else if let personalPeerName = personalPeerName { + globalTitle = presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string + } else { + globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone + } + items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in + c?.dismiss(completion: { + let _ = context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + }) + }))) + } + + if actions.options.contains(.deleteLocally) { + var localOptionText = presentationData.strings.Conversation_DeleteMessagesForMe + if context.account.peerId == message.id.peerId { + if messageIds.count == 1 { + localOptionText = presentationData.strings.Conversation_Moderate_Delete + } else { + localOptionText = presentationData.strings.Conversation_DeleteManyMessages + } + } + items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in + c?.dismiss(completion: { + let _ = context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).startStandalone() + }) + }))) + } + } + + return ContextController.Items(content: .list(items)) + }, minHeight: nil, animated: true) + } + })) + ) + } + + guard !items.isEmpty else { + return + } + + let contextController = ContextController(presentationData: presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture) + self.getParentController()?.presentInGlobalOverlay(contextController) + }) + } +} + +private final class OverlayAudioPlayerContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentNode: ContextExtractedContentContainingNode + + init(contentNode: ContextExtractedContentContainingNode) { + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 7e19dabd10..d18bc300b3 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -19,6 +19,11 @@ import ContextUI import SliderContextItem import UndoUI import MarqueeComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import Markdown +import TextFormat private func normalizeValue(_ value: CGFloat) -> CGFloat { return round(value * 10.0) / 10.0 @@ -106,8 +111,8 @@ private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat { return size.width } -private let titleFont = Font.semibold(18.0) -private let descriptionFont = Font.regular(18.0) +private let titleFont = Font.semibold(19.0) +private let descriptionFont = Font.regular(17.0) private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, presentationData: PresentationData) -> (NSAttributedString?, NSAttributedString?, Bool, NSAttributedString?) { var titleString: NSAttributedString? @@ -141,6 +146,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private let account: Account private let engine: TelegramEngine private var presentationData: PresentationData + private let chatLocation: ChatLocation + private let source: ChatHistoryListSource private let backgroundNode: ASImageNode @@ -154,6 +161,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private let shareNode: HighlightableButtonNode private let artistButton: HighlightTrackingButtonNode + private var profileAudio: ComponentView? + private var cachedChevronImage: (UIImage, PresentationTheme)? + private let scrubberNode: MediaPlayerScrubbingNode private let leftDurationLabel: MediaPlayerTimeTextNode private let rightDurationLabel: MediaPlayerTimeTextNode @@ -181,12 +191,18 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let separatorNode: ASDisplayNode + private let sectionBackground: ASDisplayNode + private let sectionTitle: ComponentView + var isExpanded = false var updateIsExpanded: (() -> Void)? var requestCollapse: (() -> Void)? var requestShare: ((MessageId) -> Void)? var requestSearchByArtist: ((String) -> Void)? + var requestSaveToProfile: ((FileMediaReference) -> Void)? + var requestRemoveFromProfile: ((FileMediaReference) -> Void)? + var requestLayout: ((ContainedViewLayoutTransition) -> Void)? var updateOrder: ((MusicPlaybackSettingsOrder) -> Void)? var control: ((SharedMediaPlayerControlAction) -> Void)? @@ -197,10 +213,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var displayData: SharedMediaPlaybackDisplayData? private var currentAlbumArtInitialized = false private var currentAlbumArt: SharedMediaPlaybackAlbumArt? - private var currentFileReference: FileMediaReference? + private(set) var currentFileReference: FileMediaReference? private var statusDisposable: Disposable? private var chapterDisposable: Disposable? + private var peerName: String? + private var peerDisposable: Disposable? + private var previousCaption: NSAttributedString? private var chaptersPromise = ValuePromise<[MediaPlayerScrubbingChapter]>([]) private var currentChapter: MediaPlayerScrubbingChapter? @@ -215,13 +234,15 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var currentDuration: Double = 0.0 private var currentPosition: Double = 0.0 - private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)? + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, hasSectionHeader: Bool, savedMusic: Bool?)? - init(account: Account, engine: TelegramEngine, accountManager: AccountManager, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError>) { + init(account: Account, engine: TelegramEngine, accountManager: AccountManager, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError>, chatLocation: ChatLocation, source: ChatHistoryListSource) { self.accountManager = accountManager self.account = account self.engine = engine self.presentationData = presentationData + self.chatLocation = chatLocation + self.source = source self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -240,7 +261,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.titleNode.displaysAsynchronously = false self.title = ComponentView() - + self.descriptionNode = TextNode() self.descriptionNode.isUserInteractionEnabled = false self.descriptionNode.displaysAsynchronously = false @@ -293,6 +314,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.separatorNode.isLayerBacked = true self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor + self.sectionBackground = ASDisplayNode() + self.sectionBackground.backgroundColor = presentationData.theme.chatList.sectionHeaderFillColor + + self.sectionTitle = ComponentView() + super.init() self.addSubnode(self.backgroundNode) @@ -318,6 +344,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.addSubnode(self.playPauseButton) self.playPauseButton.addSubnode(self.playPauseIconNode) + self.addSubnode(self.sectionBackground) self.addSubnode(self.separatorNode) let accountId = account.id @@ -369,7 +396,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { strongSelf.infoNodePushed = infoNodePushed if let layout = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring)) + let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, hasSectionHeader: layout.4, savedMusic: layout.5, transition: .animated(duration: 0.35, curve: .spring)) } } }) @@ -379,6 +406,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { guard let strongSelf = self else { return } + var itemUpdated = false var valueItemId: SharedMediaPlaylistItemId? if let (_, value, _) = value, case let .state(state) = value { valueItemId = state.item.id @@ -386,6 +414,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) { strongSelf.currentItemId = valueItemId strongSelf.scrubberNode.ignoreSeekId = nil + itemUpdated = true } var rateButtonIsHidden = true @@ -445,7 +474,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if duration != strongSelf.currentDuration && !duration.isZero { strongSelf.currentDuration = duration if let layout = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate) + let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, hasSectionHeader: layout.4, savedMusic: layout.5, transition: .immediate) } } @@ -485,8 +514,25 @@ final class OverlayPlayerControlsNode: ASDisplayNode { strongSelf.shareNode.isHidden = !canShare } + + if itemUpdated { + strongSelf.requestLayout?(.animated(duration: 0.2, curve: .easeInOut)) + } }) + if case .custom = self.source, case let .peer(peerId) = self.chatLocation, peerId != account.peerId { + self.peerDisposable = (engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.peerName = peer.compactDisplayTitle + if let layout = self.validLayout { + let _ = self.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, hasSectionHeader: layout.4, savedMusic: layout.5, transition: .immediate) + } + }) + } + self.chapterDisposable = combineLatest(queue: Queue.mainQueue(), mappedStatus, self.chaptersPromise.get()) .startStrict(next: { [weak self] status, chapters in if let strongSelf = self, status.duration > 1.0, chapters.count > 0 { @@ -533,7 +579,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { strongSelf.infoNode.attributedText = NSAttributedString(string: chapter.title, font: Font.regular(13.0), textColor: strongSelf.presentationData.theme.list.itemSecondaryTextColor) if let layout = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate) + let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, hasSectionHeader: layout.4, savedMusic: layout.5, transition: .immediate) } } } @@ -578,6 +624,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.statusDisposable?.dispose() self.chapterDisposable?.dispose() self.scrubbingDisposable?.dispose() + self.peerDisposable?.dispose() } override func didLoad() { @@ -704,14 +751,15 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.updateRateButton(rate) } self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor + self.sectionBackground.backgroundColor = presentationData.theme.chatList.sectionHeaderFillColor } private func updateLabels(transition: ContainedViewLayoutTransition) { - guard let (width, leftInset, rightInset, maxHeight) = self.validLayout else { + guard let (width, leftInset, rightInset, maxHeight, _, _) = self.validLayout else { return } - let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded) + let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded, hasSectionHeader: false, savedMusic: nil) let sideInset: CGFloat = 20.0 @@ -740,13 +788,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode { MarqueeComponent(attributedText: titleString ?? NSAttributedString()) ), environment: {}, - containerSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset + MarqueeComponent.innerPadding, height: CGFloat.greatestFiniteMagnitude) + containerSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude) ) if let titleView = self.title.view { if titleView.superview == nil { self.view.addSubview(titleView) } - transition.updateFrame(view: titleView, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleSize.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset) - MarqueeComponent.innerPadding, y: infoVerticalOrigin + 1.0), size: titleSize)) + transition.updateFrame(view: titleView, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleSize.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin), size: titleSize)) } let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) @@ -755,7 +803,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 1.0), size: titleLayout.size)) let _ = titleApply() - let descriptionFrame = CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 24.0), size: descriptionLayout.size) + let descriptionFrame = CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 25.0), size: descriptionLayout.size) transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame) let _ = descriptionApply() @@ -819,20 +867,30 @@ final class OverlayPlayerControlsNode: ASDisplayNode { } static let basePanelHeight: CGFloat = 220.0 + static let sectionHeaderHeight: CGFloat = 28.0 - static func heightForLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isExpanded: Bool) -> CGFloat { + static func heightForLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isExpanded: Bool, hasSectionHeader: Bool, savedMusic: Bool?) -> CGFloat { var panelHeight: CGFloat = OverlayPlayerControlsNode.basePanelHeight if isExpanded { let sideInset: CGFloat = 20.0 panelHeight += width - leftInset - rightInset - sideInset * 2.0 + 24.0 } - return min(panelHeight, maxHeight) + var height = min(panelHeight, maxHeight) + if hasSectionHeader { + height += sectionHeaderHeight + } + if let savedMusic { + height += savedMusic ? 38.0 : 70.0 + } + return height } - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, maxHeight) + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, hasSectionHeader: Bool, savedMusic: Bool?, transition: ContainedViewLayoutTransition) -> CGFloat { + let previousSavedMusic = self.validLayout?.savedMusic + self.validLayout = (width, leftInset, rightInset, maxHeight, hasSectionHeader, savedMusic) - let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded) + let finalPanelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded, hasSectionHeader: hasSectionHeader, savedMusic: savedMusic) + let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded, hasSectionHeader: false, savedMusic: nil) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel))) @@ -841,6 +899,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let sideInset: CGFloat = 20.0 let sideButtonsInset: CGFloat = sideInset + 36.0 + let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0 self.updateLabels(transition: transition) @@ -941,7 +1000,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset - 10.0), size: CGSize(width: 24.0, height: 44.0))) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: finalPanelHeight + 8.0))) let buttonSize = CGSize(width: 64.0, height: 64.0) let buttonsWidth = min(width - leftInset - rightInset - sideButtonsInset * 2.0, 320.0) @@ -956,8 +1015,160 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let playPauseFrame = CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize) transition.updateFrame(node: self.playPauseButton, frame: playPauseFrame) transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: -6.0, y: -6.0), size: CGSize(width: 76.0, height: 76.0))) + + var sectionHeaderTransition = transition + if self.sectionTitle.view?.superview == nil { + sectionHeaderTransition = .immediate + } - return panelHeight + sectionHeaderTransition.updateFrame(node: self.sectionBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: finalPanelHeight - OverlayPlayerControlsNode.sectionHeaderHeight), size: CGSize(width: width, height: OverlayPlayerControlsNode.sectionHeaderHeight))) + + self.separatorNode.isHidden = hasSectionHeader + + if hasSectionHeader { + //TODO:localize + var sectionTitle = "AUDIO IN THIS CHAT" + if let peerName = self.peerName { + sectionTitle = "\(peerName)'S PLAYLIST" + } else if case .custom = self.source { + sectionTitle = "YOUR PLAYLIST" + } + let sectionTitleSize = self.sectionTitle.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: sectionTitle, font: Font.regular(13.0), textColor: self.presentationData.theme.chatList.sectionHeaderTextColor))) + ), + environment: {}, + containerSize: CGSize(width: width, height: OverlayPlayerControlsNode.sectionHeaderHeight) + ) + if let sectionTitleView = self.sectionTitle.view { + if sectionTitleView.superview == nil { + self.view.addSubview(sectionTitleView) + } + sectionTitleView.bounds = CGRect(origin: .zero, size: sectionTitleSize) + sectionHeaderTransition.updateFrame(view: sectionTitleView, frame: CGRect(origin: CGPoint(x: leftInset + 16.0, y: finalPanelHeight - OverlayPlayerControlsNode.sectionHeaderHeight + 6.0 + UIScreenPixel), size: sectionTitleSize)) + } + } else if let sectionTitleView = self.sectionTitle.view, sectionTitleView.superview != nil { + sectionTitleView.removeFromSuperview() + } + + if let savedMusic { + var profileAudioTransition = transition + var animateIn = false + if previousSavedMusic != savedMusic, let profileAudio { + self.profileAudio = nil + if let profileAudioView = profileAudio.view { + if transition.isAnimated { + profileAudioView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.25) + profileAudioView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + profileAudioView.removeFromSuperview() + }) + animateIn = true + } else { + profileAudioView.removeFromSuperview() + } + } + } + + if self.profileAudio == nil { + profileAudioTransition = .immediate + } + let profileAudio: ComponentView = self.profileAudio ?? { + let componentView = ComponentView() + self.profileAudio = componentView + return componentView + }() + + let profileAudioComponent: AnyComponent + var profileAudioOffset: CGFloat = 0.0 + if savedMusic { + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== self.presentationData.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: self.presentationData.theme.list.itemAccentColor)!, self.presentationData.theme) + } + let textFont = Font.regular(13.0) + let textColor = self.presentationData.theme.list.itemSecondaryTextColor + let linkColor = self.presentationData.theme.list.itemAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let attributedString = parseMarkdownIntoAttributedString("This audio is visible on your profile. [Remove >]()", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) + } + profileAudioComponent = AnyComponent(MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + if let file = self?.currentFileReference { + self?.requestRemoveFromProfile?(file) + } + } + } + )) + profileAudioOffset = 18.0 + } else { + //TODO:localize + profileAudioComponent = AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: self.presentationData.theme.list.itemCheckColors.fillColor, + foreground: self.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: self.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: "Peer Info/SaveMusic", tintColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Add to Profile", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor))) + )) + ], spacing: 8.0) + )), + action: { [weak self] in + if let file = self?.currentFileReference { + self?.requestSaveToProfile?(file) + } + } + )) + } + + let profileAudioSize = profileAudio.update( + transition: .immediate, + component: profileAudioComponent, + environment: {}, + containerSize: CGSize(width: width - leftInset - rightInset - 32.0, height: 50.0) + ) + let profileAudioOrigin: CGFloat = finalPanelHeight + profileAudioOffset - (hasSectionHeader ? OverlayPlayerControlsNode.sectionHeaderHeight : 0.0) - 42.0 - floorToScreenPixels(profileAudioSize.height / 2.0) + let profileAudioFrame = CGRect(origin: CGPoint(x: floor((width - profileAudioSize.width) / 2.0), y: profileAudioOrigin), size: profileAudioSize) + if let profileAudioView = profileAudio.view { + if profileAudioView.superview == nil { + self.view.addSubview(profileAudioView) + + if animateIn { + profileAudioView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + profileAudioView.layer.animateScale(from: 0.0, to: 1.0, duration: 0.3) + } + } + profileAudioTransition.updateFrame(view: profileAudioView, frame: profileAudioFrame) + } + } + + return finalPanelHeight } func collapse() { diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 9eb5ba1858..e919761504 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -427,7 +427,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.messagesLocation = location switch self.messagesLocation { - case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, messageId, _): + case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _): self.loadItem(anchor: .messageId(messageId), navigation: .later, reversed: self.order == .reversed) case let .recentActions(message): self.loadingItem = false @@ -521,7 +521,8 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } self.stateValue.set(.single(SharedMediaPlaylistState(loading: self.loadingItem, playedToEnd: self.playedToEnd, item: item, nextItem: nextItem, previousItem: previousItem, order: self.order, looping: self.looping))) - if item?.message.id != self.currentlyObservedMessageId { + if case .custom = self.messagesLocation { + } else if item?.message.id != self.currentlyObservedMessageId { self.currentlyObservedMessageId = item?.message.id if let id = item?.message.id { self.currentlyObservedMessageDisposable.set((self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: id)) @@ -593,7 +594,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { strongSelf.updateState() } })) - case let .custom(messages, at, _): + case let .custom(messages, _, at, _): self.navigationDisposable.set((messages |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] messages in @@ -769,7 +770,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.loadingItem = false self.currentItem = (message, []) self.updateState() - case let .custom(messages, _, loadMore): + case let .custom(messages, _, _, loadMore): let inputIndex: Signal let looping = self.looping switch self.order { diff --git a/submodules/TelegramUI/Sources/SaveMediaToFiles.swift b/submodules/TelegramUI/Sources/SaveMediaToFiles.swift new file mode 100644 index 0000000000..7ad5919923 --- /dev/null +++ b/submodules/TelegramUI/Sources/SaveMediaToFiles.swift @@ -0,0 +1,128 @@ +import Foundation +import AVFoundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import OverlayStatusController +import LegacyMediaPickerUI +import SaveToCameraRoll +import PresentationDataUtils + +func saveMediaToFiles(context: AccountContext, fileReference: FileMediaReference, present: @escaping (ViewController, Any?) -> Void) -> Disposable { + var title: String? + var performer: String? + for attribute in fileReference.media.attributes { + if case let .Audio(_, _, titleValue, performerValue, _) = attribute { + if let titleValue, !titleValue.isEmpty { + title = titleValue + } + if let performerValue, !performerValue.isEmpty { + performer = performerValue + } + } + } + + var signal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: fileReference.abstract) + + var cancelImpl: (() -> Void)? + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + + let progressDisposable = progressSignal.startStrict() + + let disposable = MetaDisposable() + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { [weak disposable] in + disposable?.set(nil) + } + disposable.set((signal + |> deliverOnMainQueue).startStrict(next: { state, _ in + switch state { + case .progress: + break + case let .data(data): + if data.complete { + var symlinkPath = data.path + ".mp3" + if fileSize(symlinkPath) != nil { + try? FileManager.default.removeItem(atPath: symlinkPath) + } + let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath) + + let audioUrl = URL(fileURLWithPath: symlinkPath) + let audioAsset = AVURLAsset(url: audioUrl) + + var fileExtension = "mp3" + if let filename = fileReference.media.fileName { + if let dotIndex = filename.lastIndex(of: ".") { + fileExtension = String(filename[filename.index(after: dotIndex)...]) + } + } + + var nameComponents: [String] = [] + if let title { + if let performer { + nameComponents.append(performer) + } + nameComponents.append(title) + } else { + var artist: String? + var title: String? + for data in audioAsset.commonMetadata { + if data.commonKey == .commonKeyArtist { + artist = data.stringValue + } + if data.commonKey == .commonKeyTitle { + title = data.stringValue + } + } + if let artist, !artist.isEmpty { + nameComponents.append(artist) + } + if let title, !title.isEmpty { + nameComponents.append(title) + } + if nameComponents.isEmpty, var filename = fileReference.media.fileName { + if let dotIndex = filename.lastIndex(of: ".") { + filename = String(filename[.. ViewController & OverlayAudioPlayerController { - return OverlayAudioPlayerControllerImpl(context: context, chatLocation: chatLocation, type: type, initialMessageId: initialMessageId, initialOrder: initialOrder, playlistLocation: playlistLocation, parentNavigationController: parentNavigationController) + public func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?, updateMusicSaved: ((FileMediaReference, Bool) -> Void)?, reorderSavedMusic: ((FileMediaReference, FileMediaReference?) -> Void)?) -> ViewController & OverlayAudioPlayerController { + if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case .custom = playlistLocation { + context.sharedContext.mediaManager.setPlaylist((context, PeerMessagesMediaPlaylist(context: context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.play)) + } + return OverlayAudioPlayerControllerImpl(context: context, chatLocation: chatLocation, type: type, initialMessageId: initialMessageId, initialOrder: initialOrder, playlistLocation: playlistLocation, parentNavigationController: parentNavigationController, updateMusicSaved: updateMusicSaved, reorderSavedMusic: reorderSavedMusic) } public func makeTempAccountContext(account: Account) -> AccountContext {