This commit is contained in:
Isaac 2025-08-22 15:31:35 +02:00
commit 290d955b59
51 changed files with 2194 additions and 741 deletions

View file

@ -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<ChatLocationContextHolder?>, 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<PresentationData, NoError>)?, 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

View file

@ -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>)
}

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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<FileMediaResourceStatus, NoError>?
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
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<FileMediaResourceStatus, NoError> 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))

View file

@ -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
}

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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)

View file

@ -9362,6 +9362,21 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func checkCanSendGift(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.CheckCanSendGiftResult>) {
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<Api.payments.CheckedGiftCode>) {
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<Api.users.SavedMusic>) {
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()

View file

@ -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 {

View file

@ -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

View file

@ -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<TelegramMediaFile, RevalidateMediaReferenceError> {
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<TelegramMediaFile, RevalidateMediaReferenceError> in
if let result {
return .single(result)
} else {
return .fail(.generic)
}
}).start(next: { value in
next(value)
}, error: { _ in
error(.generic)
})
}) |> mapToSignal { next -> Signal<TelegramMediaFile, RevalidateMediaReferenceError> 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<RevalidatedMediaResource, RevalidateMediaReferenceError> 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 {

View file

@ -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?

View file

@ -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<T: Media> {
case customEmoji
case story
case starsTransaction
case savedMusic
}
case standalone(media: T)
@ -607,6 +625,7 @@ public enum MediaReference<T: Media> {
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<T: Media> {
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<T: Media> {
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<T: Media> {
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<T: Media> {
return media
case let .starsTransaction(_, media):
return media
case let .savedMusic(_, media):
return media
}
}
@ -822,6 +854,8 @@ public enum MediaReference<T: Media> {
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)
}
}

View file

@ -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)

View file

@ -2415,6 +2415,36 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct ProfileMainTab: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<TelegramProfileTab>
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<String>

View file

@ -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<Api.InputPeer?, NoError>
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<Api.InputStorePaymentPurpose, NoError> in
guard let inputUser = apiInputUser(peer) else {
return .complete()

View file

@ -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

View file

@ -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<CanSendGiftResult, NoError> {
return account.network.request(Api.functions.payments.checkCanSendGift(giftId: giftId))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.CheckCanSendGiftResult?, NoError> 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<StarG
|> mapToSignal { result -> Signal<StarGift.UniqueGift?, NoError> 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

View file

@ -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
)
}
}

View file

@ -141,6 +141,10 @@ public extension TelegramEngine {
return _internal_starGiftUpgradePreview(account: self.account, giftId: giftId)
}
public func checkCanSendStarGift(giftId: Int64) -> Signal<CanSendGiftResult, NoError> {
return _internal_checkCanSendStarGift(account: self.account, giftId: giftId)
}
public func getUniqueStarGift(slug: String) -> Signal<StarGift.UniqueGift?, NoError> {
return _internal_getUniqueStarGift(account: self.account, slug: slug)
}

View file

@ -49,6 +49,31 @@ public final class SavedMusicIdsList: Codable, Equatable {
}
}
func _internal_getSavedMusicById(postbox: Postbox, network: Network, peer: PeerReference, file: TelegramMediaFile) -> Signal<TelegramMediaFile?, NoError> {
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<Api.users.SavedMusic?, NoError> 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<Set<Int64>?, 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<Never, AddSavedMusicError> {
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<Never, AddSavedMusicError> {
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<Never, NoError> {
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() {

View file

@ -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<NetworkRequestResult<Api.messages.WebPagePreview>, NoError> in
return .single(.result(.webPagePreview(media: .messageMediaEmpty, users: [])))
return .single(.result(.webPagePreview(media: .messageMediaEmpty, chats: [], users: [])))
}
|> mapToSignal { result -> Signal<WebpagePreviewWithProgressResult, NoError> 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<WebpagePreviewWithProgressResult, NoError> 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 {

View file

@ -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()
}

View file

@ -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 {

View file

@ -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<ChatLocationContextHolder?>, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, privacySettings: Signal<AccountPrivacySettings?, NoError>, forceHasGifts: Bool) -> Signal<PeerInfoScreenData, NoError> {
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<ChatLocationContextHolder?>,
sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?,
privacySettings: Signal<AccountPrivacySettings?, NoError>,
forceHasGifts: Bool
) -> Signal<PeerInfoScreenData, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings)
|> mapToSignal { inputData -> Signal<PeerInfoScreenData, NoError> in
let wasUpgradedGroup = Atomic<Bool?>(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
))
}
}

View file

@ -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<Empty>?
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<Empty>()
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

View file

@ -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<PeerId, Peer>()
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

View file

@ -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)

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "profilemusic_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "profilemusic_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -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<Never, NoError> { [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[..<dotIndex])
}
nameComponents.append(filename)
}
}
if !nameComponents.isEmpty {
try? FileManager.default.removeItem(atPath: symlinkPath)
let fileName = "\(nameComponents.joined(separator: " ")).\(fileExtension)"
symlinkPath = symlinkPath.replacingOccurrences(of: audioUrl.lastPathComponent, with: fileName)
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
}
let url = URL(fileURLWithPath: symlinkPath)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, in: .window(.root))
}
}
}))
)
)
})
}, openNoAdsDemo: { [weak self] in
guard let self else {

View file

@ -553,7 +553,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return (messages, Int32(messages.count), 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)
case let .reply(reply):
let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId))
|> 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

View file

@ -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<Int32>?, Set<MessageId>), 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)

View file

@ -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)

View file

@ -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())

View file

@ -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<Bool>()
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<Set<Int64>?>()
private var savedIds: Set<Int64>?
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<ChatLocationContextHolder?>(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<Bool, NoError> 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<ChatLocationContextHolder?>(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)
}
}

View file

@ -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<Empty>?
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<Empty>
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<TelegramAccountManagerTypes>, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError>) {
init(account: Account, engine: TelegramEngine, accountManager: AccountManager<TelegramAccountManagerTypes>, 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<Empty>()
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<Empty>()
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<Empty> = self.profileAudio ?? {
let componentView = ComponentView<Empty>()
self.profileAudio = componentView
return componentView
}()
let profileAudioComponent: AnyComponent<Empty>
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() {

View file

@ -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<MessageIndex, NoError>
let looping = self.looping
switch self.order {

View file

@ -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<Never, NoError> { 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[..<dotIndex])
}
nameComponents.append(filename)
}
}
if !nameComponents.isEmpty {
try? FileManager.default.removeItem(atPath: symlinkPath)
let fileName = "\(nameComponents.joined(separator: " ")).\(fileExtension)"
symlinkPath = symlinkPath.replacingOccurrences(of: audioUrl.lastPathComponent, with: fileName)
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
}
let url = URL(fileURLWithPath: symlinkPath)
let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
present(controller, nil)
}
}
}))
return disposable
}

View file

@ -1786,8 +1786,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
})
}
public func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> 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 {