[WIP] Saved music

This commit is contained in:
Ilya Laktyushin 2025-08-21 09:08:35 +04:00
parent 66a614a9a4
commit 0bac271cdb
28 changed files with 1415 additions and 295 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

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

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

@ -127,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 {
@ -144,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
@ -283,7 +286,7 @@ public final class ProfileSavedMusicContext {
self.account = account
self.peerId = peerId
self.reload()
self.loadMore()
}
deinit {
@ -367,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

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

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

@ -4414,29 +4414,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
@ -4444,108 +4430,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

@ -215,7 +215,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
@ -238,7 +238,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):
@ -272,7 +272,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
@ -295,7 +295,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):
@ -329,11 +329,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) {
@ -1384,8 +1384,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
@ -1902,7 +1904,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)
@ -2293,7 +2295,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 {