mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
[WIP] Saved music
This commit is contained in:
parent
66a614a9a4
commit
0bac271cdb
@ -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
|
||||
|
@ -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>)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profilemusic_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/profilemusic_20.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SaveMusic.imageset/profilemusic_20.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profilemusic_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/profilemusic_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/SavedMusic.imageset/profilemusic_30.pdf
vendored
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/anim_gift.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_gift.tgs
Normal file
Binary file not shown.
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
128
submodules/TelegramUI/Sources/SaveMediaToFiles.swift
Normal file
128
submodules/TelegramUI/Sources/SaveMediaToFiles.swift
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user