diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f9c4d79227..25515851e0 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1389,7 +1389,7 @@ public protocol SharedAccountContext: AnyObject { func makeBirthdayPrivacyController(context: AccountContext, settings: Promise, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController - func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController + func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference]) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index 21040b9100..2181367a5f 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/LiquidLens", "//submodules/TelegramUI/Components/TabSelectionRecognizer", + "//submodules/TelegramBaseController", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index d465d53a43..c2ba110fd8 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -1387,7 +1387,7 @@ public class AttachmentController: ViewController, MinimizableController { // let previousHasButton = self.hasButton let hasButton = self.panel.isButtonVisible && !self.isDismissing self.hasButton = hasButton - if let controller = self.controller, controller.buttons.count > 1 || controller.hasTextInput { + if let controller = self.controller, controller.buttons.count > 1 || controller.hasTextInput || self.panel.hasMediaAccessoryPanel { hasPanel = true } if !self.isPanelVisible { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index d33208c5f0..57dcc57c74 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -7,6 +7,7 @@ import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData +import TelegramUIPreferences import AccountContext import AttachmentTextInputPanelNode import ChatPresentationInterfaceState @@ -25,6 +26,10 @@ import ReactionSelectionNode import TopMessageReactions import GlassBackgroundComponent import LiquidLens +import UniversalMediaPlayer +import TelegramBaseController +import OverlayStatusController +import UndoUI import TabSelectionRecognizer import EmojiTextAttachmentView @@ -233,10 +238,10 @@ private final class AttachButtonComponent: CombinedComponent { case .sticker: //TODO:localize name = "Sticker" - imageName = "Chat/Attach Menu/Gift" + imageName = "Chat/Attach Menu/Sticker" case .emoji: name = "Emoji" - imageName = "Chat/Attach Menu/Reply" + imageName = "Chat/Attach Menu/Emoji" case .audio: name = "Audio" imageName = "Chat/Attach Menu/Audio" @@ -967,8 +972,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog private var updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? private var peerDisposable: Disposable? + private var mediaStatusDisposable: Disposable? + private var playlistPreloadDisposable: Disposable? private var iconDisposables: [MediaId: Disposable] = [:] + private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? + private var playlistLocation: SharedMediaPlaylistLocation? + private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? + private var dismissingMediaAccessoryPanel: ASDisplayNode? private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -1014,6 +1025,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog var isButtonVisible: Bool { return self.mainButtonState.isVisible || self.secondaryButtonState.isVisible } + var hasMediaAccessoryPanel: Bool { + if case .glass = self.panelStyle { + return self.playlistStateAndType != nil + } else { + return false + } + } private var validLayout: ContainerViewLayout? private var scrollLayout: (width: CGFloat, contentSize: CGSize)? @@ -1433,6 +1451,61 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog return nil }, statuses: nil) + if case .glass = style { + self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState + |> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in + guard let (account, stateOrLoading, type) = playlistStateAndType, type == .music else { + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + switch stateOrLoading { + case let .state(state): + return .single((account, state, type)) + case .loading: + return .single(nil) |> delay(0.2, queue: .mainQueue()) + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] playlistStateAndType in + guard let strongSelf = self else { + return + } + let hadMediaAccessoryPanel = strongSelf.hasMediaAccessoryPanel + if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) || + !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) || + !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) || + strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || + strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 { + + if let playlistStateAndType { + strongSelf.playlistStateAndType = ( + playlistStateAndType.1.item, + playlistStateAndType.1.previousItem, + playlistStateAndType.1.nextItem, + playlistStateAndType.1.order, + playlistStateAndType.2, + playlistStateAndType.0 + ) + strongSelf.playlistLocation = playlistStateAndType.1.playlistLocation + } else { + strongSelf.playlistStateAndType = nil + strongSelf.playlistLocation = nil + } + + if hadMediaAccessoryPanel != strongSelf.hasMediaAccessoryPanel { + strongSelf.requestLayout() + } else if let layout = strongSelf.validLayout { + let _ = strongSelf.update( + layout: layout, + buttons: strongSelf.buttons, + isSelecting: strongSelf.isSelecting, + selectionCount: strongSelf.selectionCount, + elevateProgress: strongSelf.elevateProgress, + transition: .animated(duration: 0.4, curve: .spring) + ) + } + } + }) + } + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { @@ -1441,6 +1514,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog strongSelf.backgroundNode.updateColor(color: strongSelf.customBottomPanelBackgroundColor ?? presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) strongSelf.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor strongSelf.selectionNode.backgroundColor = presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05) + if let (mediaAccessoryPanel, _) = strongSelf.mediaAccessoryPanel { + strongSelf.mediaAccessoryPanel = nil + mediaAccessoryPanel.removeFromSupernode() + } + if let dismissingMediaAccessoryPanel = strongSelf.dismissingMediaAccessoryPanel { + strongSelf.dismissingMediaAccessoryPanel = nil + dismissingMediaAccessoryPanel.removeFromSupernode() + } strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) }) @@ -1480,6 +1561,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog deinit { self.presentationDataDisposable?.dispose() self.peerDisposable?.dispose() + self.mediaStatusDisposable?.dispose() + self.playlistPreloadDisposable?.dispose() for (_, disposable) in self.iconDisposables { disposable.dispose() } @@ -1512,6 +1595,246 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.onSecondaryButtonPressed() } + private func mediaPlaybackStatusSignal() -> Signal { + let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState + |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in + guard let value, value.2 == .music else { + return .single(nil) + } + switch value.1 { + case .state: + return .single(value) + case .loading: + return .single(value) |> delay(0.1, queue: .mainQueue()) + } + } + + return delayedStatus + |> map { state -> MediaPlayerStatus in + if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { + return state.status + } else { + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) + } + } + } + + private func dismissMediaAccessoryPanel(transition: ContainedViewLayoutTransition) { + guard let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel else { + return + } + self.mediaAccessoryPanel = nil + self.dismissingMediaAccessoryPanel = mediaAccessoryPanel + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in + mediaAccessoryPanel?.removeFromSupernode() + if let self, self.dismissingMediaAccessoryPanel === mediaAccessoryPanel { + self.dismissingMediaAccessoryPanel = nil + } + }) + } + + private func presentAudioRateTooltip(baseRate: AudioPlaybackRate, changeType: MediaNavigationAccessoryPanel.ChangeType) { + guard let controller = self.controller else { + return + } + + var hasTooltip = false + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + hasTooltip = true + controller.dismissWithCommitAction() + } + return true + }) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String? + let rate: CGFloat? + if case let .sliderCommit(previousValue, newValue) = changeType { + let value = String(format: "%0.1f", baseRate.doubleValue) + if baseRate == .x1 { + text = presentationData.strings.Conversation_AudioRateTooltipNormal + } else { + text = presentationData.strings.Conversation_AudioRateTooltipCustom(value).string + } + if newValue > previousValue { + rate = .infinity + } else if newValue < previousValue { + rate = -.infinity + } else { + rate = nil + } + } else if baseRate == .x1 { + text = presentationData.strings.Conversation_AudioRateTooltipNormal + rate = 1.0 + } else if baseRate == .x1_5 { + text = presentationData.strings.Conversation_AudioRateTooltip15X + rate = 1.5 + } else if baseRate == .x2 { + text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp + rate = 2.0 + } else { + text = nil + rate = nil + } + + var showTooltip = true + if case .sliderChange = changeType { + showTooltip = false + } + + if let rate, let text, showTooltip { + controller.present( + UndoOverlayController( + presentationData: presentationData, + content: .audioRate( + rate: rate, + text: text + ), + elevatedLayout: false, + animateInAsReplacement: hasTooltip, + action: { _ in + return true + } + ), + in: .current + ) + } + } + + private func openCurrentMusicPlayer() { + guard let (state, _, _, order, type, account) = self.playlistStateAndType, + let id = state.id as? PeerMessagesMediaPlaylistItemId, + let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation else { + return + } + + guard type == .music else { + self.context.sharedContext.navigateToChat(accountId: self.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) + return + } + + if case .custom = playlistLocation { + let controllerContext: AccountContext + if account.id == self.context.account.id { + controllerContext = self.context + } else { + controllerContext = self.context.sharedContext.makeTempAccountContext(account: account) + } + let controller = self.context.sharedContext.makeOverlayAudioPlayerController( + context: controllerContext, + chatLocation: .peer(id: id.messageId.peerId), + type: type, + initialMessageId: id.messageId, + initialOrder: order, + playlistLocation: playlistLocation, + parentNavigationController: self.controller?.navigationController as? NavigationController + ) + self.view.window?.endEditing(true) + self.present(controller) + } + } + + private func makeMediaAccessoryPanel() -> MediaNavigationAccessoryPanel { + let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, presentationData: self.presentationData, displayBackground: false, customTintColor: self.presentationData.theme.rootController.tabBar.textColor) + mediaAccessoryPanel.getController = { [weak self] in + return self?.controller + } + mediaAccessoryPanel.presentInGlobalOverlay = { [weak self] controller in + self?.presentInGlobalOverlay(controller) + } + mediaAccessoryPanel.close = { [weak self] in + if let self, let (_, _, _, _, type, _) = self.playlistStateAndType { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) + } + } + mediaAccessoryPanel.setRate = { [weak self] rate, changeType in + guard let self else { + return + } + let _ = (self.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings)?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings + + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in + return PreferencesEntry(settings.withUpdatedVoicePlaybackRate(rate)) + }) + return rate + } + |> deliverOnMainQueue).startStandalone(next: { [weak self] baseRate in + guard let self, let (_, _, _, _, type, _) = self.playlistStateAndType else { + return + } + self.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type) + self.presentAudioRateTooltip(baseRate: baseRate, changeType: changeType) + }) + } + mediaAccessoryPanel.togglePlayPause = { [weak self] in + if let self, let (_, _, _, _, type, _) = self.playlistStateAndType { + self.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) + } + } + mediaAccessoryPanel.playPrevious = { [weak self] in + if let self, let (_, _, _, _, type, _) = self.playlistStateAndType { + self.context.sharedContext.mediaManager.playlistControl(.next, type: type) + } + } + mediaAccessoryPanel.playNext = { [weak self] in + if let self, let (_, _, _, _, type, _) = self.playlistStateAndType { + self.context.sharedContext.mediaManager.playlistControl(.previous, type: type) + } + } + mediaAccessoryPanel.tapAction = { [weak self] in + self?.openCurrentMusicPlayer() + } + return mediaAccessoryPanel + } + + private func updateMediaAccessoryPanel(frame: CGRect?, transition: ContainedViewLayoutTransition) { + guard case .glass = self.panelStyle, + let frame, + let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType else { + self.dismissMediaAccessoryPanel(transition: transition) + return + } + + let mediaAccessoryPanel: MediaNavigationAccessoryPanel + let isNewPanel: Bool + if let (currentPanel, currentType) = self.mediaAccessoryPanel, currentType == type { + mediaAccessoryPanel = currentPanel + isNewPanel = false + } else { + self.dismissMediaAccessoryPanel(transition: transition) + mediaAccessoryPanel = self.makeMediaAccessoryPanel() + mediaAccessoryPanel.frame = frame + if let dismissingMediaAccessoryPanel = self.dismissingMediaAccessoryPanel { + self.containerNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingMediaAccessoryPanel) + } else { + self.containerNode.addSubnode(mediaAccessoryPanel) + } + self.mediaAccessoryPanel = (mediaAccessoryPanel, type) + isNewPanel = true + } + + mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo + mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.mediaPlaybackStatusSignal() + switch order { + case .regular: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) + case .reversed: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) + case .random: + mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) + } + + if isNewPanel { + mediaAccessoryPanel.updateLayout(size: frame.size, leftInset: 0.0, rightInset: 0.0, isHidden: false, transition: .immediate) + mediaAccessoryPanel.animateIn(transition: transition) + } else { + transition.updateFrame(node: mediaAccessoryPanel, frame: frame) + mediaAccessoryPanel.updateLayout(size: frame.size, leftInset: 0.0, rightInset: 0.0, isHidden: false, transition: transition) + } + } + func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateAlpha(node: self.separatorNode, alpha: alpha) transition.updateAlpha(node: self.backgroundNode, alpha: alpha) @@ -2108,6 +2431,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog insets.bottom = layout.intrinsicInsets.bottom } + let topAccessoryHeight: CGFloat + if case .glass = self.panelStyle, self.playlistStateAndType != nil { + topAccessoryHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight + } else { + topAccessoryHeight = 0.0 + } + if isSelecting { self.loadTextNodeIfNeeded() } else { @@ -2127,7 +2457,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog panelTransition = .immediate } let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) - let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight) + let panelFrame = CGRect(x: 0.0, y: topAccessoryHeight, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { textInputPanelNode.frame = panelFrame } @@ -2143,7 +2473,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.updateViews(transition: .immediate) let glassPanelHeight: CGFloat = 62.0 - let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: self.buttonSize.height + insets.bottom)) + let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: self.buttonSize.height + insets.bottom + topAccessoryHeight)) + var mediaAccessoryPanelFrame: CGRect? if case .glass = self.panelStyle { let backgroundView: GlassBackgroundView let liquidLensView: LiquidLensView @@ -2184,9 +2515,10 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0 } - let panelSize = CGSize(width: isSelecting ? textPanelWidth : buttonsPanelWidth, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight) + let basePanelHeight = isSelecting ? max(0.0, textPanelHeight - 11.0) : glassPanelHeight + let panelSize = CGSize(width: isSelecting ? textPanelWidth : buttonsPanelWidth, height: basePanelHeight + topAccessoryHeight) - let cornerRadius: CGFloat = isSelecting ? min(20.0, panelSize.height * 0.5) : panelSize.height * 0.5 + let cornerRadius: CGFloat = isSelecting ? min(20.0, basePanelHeight * 0.5) : glassPanelHeight * 0.5 let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) backgroundView.update(size: panelSize, cornerRadius: cornerRadius, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: ComponentTransition(transition)) @@ -2196,7 +2528,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.lensParams = (lensPanelSize, cornerRadius) self.updateLiquidLens(transition: ComponentTransition(transition)) - transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) + transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: topAccessoryHeight + lensPanelSize.height * 0.5)) transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: CGSize(width: lensPanelSize.width - 3.0 * 2.0, height: lensPanelSize.height - 3.0 * 2.0))) transition.updatePosition(layer: backgroundView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) @@ -2207,7 +2539,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog transition.updateBounds(layer: self.selectedItemsContainer.layer, bounds: CGRect(origin: .zero, size: itemsContainerFrame.size)) transition.updatePosition(layer: self.itemsContainer.layer, position: itemsContainerFrame.center) transition.updatePosition(layer: self.selectedItemsContainer.layer, position: itemsContainerFrame.center) + + if topAccessoryHeight > 0.0 { + mediaAccessoryPanelFrame = CGRect(origin: CGPoint(x: backgroundOriginX, y: 0.0), size: CGSize(width: panelSize.width, height: topAccessoryHeight)) + } } + self.updateMediaAccessoryPanel(frame: mediaAccessoryPanelFrame, transition: transition) var containerTransition: ContainedViewLayoutTransition var containerFrame: CGRect @@ -2255,7 +2592,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: height)) } else if isSelecting { - containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom)) + containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom + topAccessoryHeight)) } else { containerFrame = bounds } @@ -2367,7 +2704,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel))) if case .glass = self.panelStyle { - transition.updateFrameAsPositionAndBounds(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.isSelecting ? panelSideInset - defaultPanelSideInset : panelSideInset, y: self.isSelecting ? -11.0 : 0.0), size: CGSize(width: layout.size.width - panelSideInset * 2.0, height: self.buttonSize.height))) + transition.updateFrameAsPositionAndBounds(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.isSelecting ? panelSideInset - defaultPanelSideInset : panelSideInset, y: topAccessoryHeight + (self.isSelecting ? -11.0 : 0.0)), size: CGSize(width: layout.size.width - panelSideInset * 2.0, height: self.buttonSize.height))) } if let progress = self.loadingProgress { @@ -2406,7 +2743,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if !self.animatingTransition { let buttonOriginX = layout.safeInsets.left + buttonSideInset - let buttonOriginY = isAnyButtonVisible || self.fromMenu ? buttonTopInset : containerFrame.height + let buttonOriginY = isAnyButtonVisible || self.fromMenu ? topAccessoryHeight + buttonTopInset : containerFrame.height var mainButtonFrame: CGRect? var secondaryButtonFrame: CGRect? if self.secondaryButtonState.isVisible && self.mainButtonState.isVisible, let position = self.secondaryButtonState.position { diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index ed761abcc4..46e3498a0d 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -1089,10 +1089,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if dateHeaderAtBottom, let header = item.header { insets.top += header.height } - if !mergedBottom, case .blocks = item.style { - insets.bottom += 35.0 - } - + let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 var verticalInset: CGFloat = 0.0 @@ -1680,7 +1677,9 @@ public final class ListMessageFileItemNode: ListMessageNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let item = self.item, case .selectable = item.selection { - if self.bounds.contains(point) { + if !self.iconButtonNode.isHidden && self.iconButtonNode.frame.contains(point) { + + } else if self.bounds.contains(point) { return self.view } } diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift index 2ac69b494d..0513500be0 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift @@ -17,14 +17,14 @@ public final class MediaNavigationAccessoryContainerNode: ASDisplayNode, ASGestu private var presentationData: PresentationData - init(context: AccountContext, presentationData: PresentationData, displayBackground: Bool) { + init(context: AccountContext, presentationData: PresentationData, displayBackground: Bool, customTintColor: UIColor? = nil) { self.displayBackground = displayBackground self.presentationData = presentationData self.backgroundNode = ASDisplayNode() self.separatorNode = ASDisplayNode() - self.headerNode = MediaNavigationAccessoryHeaderNode(context: context, presentationData: presentationData) + self.headerNode = MediaNavigationAccessoryHeaderNode(context: context, presentationData: presentationData, customTintColor: customTintColor) super.init() diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 7e5ef1c723..91ff56afd0 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -148,6 +148,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi private var strings: PresentationStrings private var dateTimeFormat: PresentationDateTimeFormat private var nameDisplayOrder: PresentationPersonNameOrder + private var customTintColor: UIColor? private let scrollNode: ASScrollNode private var initialContentOffset: CGFloat? @@ -219,13 +220,14 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi private weak var tooltipController: TooltipScreen? private let dismissedPromise = ValuePromise(false) - public init(context: AccountContext, presentationData: PresentationData) { + public init(context: AccountContext, presentationData: PresentationData, customTintColor: UIColor? = nil) { self.context = context self.theme = presentationData.theme self.strings = presentationData.strings self.dateTimeFormat = presentationData.dateTimeFormat self.nameDisplayOrder = presentationData.nameDisplayOrder + self.customTintColor = customTintColor self.scrollNode = ASScrollNode() @@ -260,13 +262,13 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.actionButton.displaysAsynchronously = false self.playPauseIconNode = PlayPauseIconNode() - self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor + self.playPauseIconNode.customColor = customTintColor ?? self.theme.rootController.navigationBar.accentTextColor - self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) + self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: customTintColor ?? self.theme.rootController.navigationBar.accentTextColor, bufferingColor: (customTintColor ?? self.theme.rootController.navigationBar.accentTextColor).withAlphaComponent(0.5), chapters: [])) self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor super.init() @@ -366,9 +368,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.rightMaskNode.image = maskImage self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) - self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor + self.playPauseIconNode.customColor = self.customTintColor ?? self.theme.rootController.navigationBar.accentTextColor self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) + self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.customTintColor ?? self.theme.rootController.navigationBar.accentTextColor, bufferingColor: (self.customTintColor ?? self.theme.rootController.navigationBar.accentTextColor).withAlphaComponent(0.5), chapters: [])) if let playbackBaseRate = self.playbackBaseRate { self.rateButton.setContent(.image(optionsRateImage(rate: playbackBaseRate.stringValue.uppercased(), color: self.theme.rootController.navigationBar.controlColor))) @@ -481,11 +483,12 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi let rateButtonSize = CGSize(width: 30.0, height: minHeight) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 27.0 - closeButtonSize.width - rateButtonSize.width - rightInset, y: -4.0), size: rateButtonSize)) - transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0))) + transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0 + 4.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0))) transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) + let originY: CGFloat = self.customTintColor != nil ? minHeight - UIScreenPixel : 0.0 + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: CGSize(width: size.width, height: UIScreenPixel))) self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight)) } diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift index 696af04465..2940823c98 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift @@ -26,8 +26,8 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode { public var getController: (() -> ViewController?)? public var presentInGlobalOverlay: ((ViewController) -> Void)? - public init(context: AccountContext, presentationData: PresentationData, displayBackground: Bool = false) { - self.containerNode = MediaNavigationAccessoryContainerNode(context: context, presentationData: presentationData, displayBackground: displayBackground) + public init(context: AccountContext, presentationData: PresentationData, displayBackground: Bool = false, customTintColor: UIColor? = nil) { + self.containerNode = MediaNavigationAccessoryContainerNode(context: context, presentationData: presentationData, displayBackground: displayBackground, customTintColor: customTintColor) super.init() diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index e46efacbcf..d6c2e40cad 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -89,7 +89,6 @@ swift_library( "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager", "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/MapResourceToAvatarSizes:MapResourceToAvatarSizes", "//submodules/TextFormat:TextFormat", "//submodules/Markdown:Markdown", diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index dbfabab361..8514be1ed1 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -8,7 +8,6 @@ import DeleteChatPeerActionSheetItem import PeerListItemComponent import LegacyComponents import LegacyUI -import WebSearchUI import MapResourceToAvatarSizes import LegacyMediaPickerUI import AvatarNode diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 5bdfb5ee80..c4ba7b067f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -9,7 +9,6 @@ import ContextUI import DeleteChatPeerActionSheetItem import UndoUI import LegacyComponents -import WebSearchUI import MapResourceToAvatarSizes import LegacyUI import LegacyMediaPickerUI @@ -475,25 +474,6 @@ extension VideoChatScreenComponent.View { mixin.forceDark = true mixin.stickersContext = LegacyPaintStickersContext(context: currentCall.accountContext) let _ = self.currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { [weak self] assetsController in - guard let self, let currentCall = self.currentCall, let environment = self.environment else { - return - } - let controller = WebSearchController(context: currentCall.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in - assetsController?.dismiss() - - guard let self else { - return - } - self.updateProfilePhoto(result) - })) - controller.navigationPresentation = .modal - environment.controller()?.push(controller) - - if fromGallery { - completion() - } - } mixin.didFinishWithImage = { [weak self] image in if let image = image { completion() diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index e5625b62f8..16b78d5194 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -26,7 +26,6 @@ import TooltipUI import LegacyUI import LegacyComponents import LegacyMediaPickerUI -import WebSearchUI import MapResourceToAvatarSizes import SolidRoundedButtonNode import AudioBlob diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index 9cfdd67955..32acbcfc38 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import SwiftSignalKit import Postbox @@ -20,6 +21,7 @@ import BundleIconComponent import EdgeEffect import SaveToCameraRoll import PeerMessagesMediaPlaylist +import ContextUI private final class AttachmentFileControllerArguments { let context: AccountContext @@ -31,8 +33,12 @@ private final class AttachmentFileControllerArguments { let expandRecentMusic: () -> Void let send: (Message) -> Void let toggleMediaPlayback: (Message) -> Void - - init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, scanDocument: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, expandRecentMusic: @escaping () -> Void, send: @escaping (Message) -> Void, toggleMediaPlayback: @escaping (Message) -> Void) { + let isSelectionActive: () -> Bool + let toggleMessageSelection: (Message) -> Void + let setMessageSelection: ([MessageId], Message?, Bool) -> Void + let openMessageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void) + + init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, scanDocument: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, expandRecentMusic: @escaping () -> Void, send: @escaping (Message) -> Void, toggleMediaPlayback: @escaping (Message) -> Void, isSelectionActive: @escaping () -> Bool, toggleMessageSelection: @escaping (Message) -> Void, setMessageSelection: @escaping ([MessageId], Message?, Bool) -> Void, openMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)) { self.context = context self.isAudio = isAudio self.openGallery = openGallery @@ -42,6 +48,10 @@ private final class AttachmentFileControllerArguments { self.expandRecentMusic = expandRecentMusic self.send = send self.toggleMediaPlayback = toggleMediaPlayback + self.isSelectionActive = isSelectionActive + self.toggleMessageSelection = toggleMessageSelection + self.setMessageSelection = setMessageSelection + self.openMessageContextAction = openMessageContextAction } } @@ -69,19 +79,19 @@ private enum AttachmentFileEntry: ItemListNodeEntry { case selectFromGallery(PresentationTheme, String) case selectFromFiles(PresentationTheme, String) case scanDocument(PresentationTheme, String) - + case savedHeader(PresentationTheme, String) - case savedFile(Int32, PresentationTheme, Message?) + case savedFile(Int32, PresentationTheme, Message?, ChatHistoryMessageSelection) case savedShowMore(PresentationTheme, String) - + case recentHeader(PresentationTheme, String) - case recentFile(Int32, PresentationTheme, Message?) + case recentFile(Int32, PresentationTheme, Message?, ChatHistoryMessageSelection) case recentShowMore(PresentationTheme, String) - + case globalHeader(PresentationTheme, String) - case globalFile(Int32, PresentationTheme, Message?) + case globalFile(Int32, PresentationTheme, Message?, ChatHistoryMessageSelection) case globalShowMore(PresentationTheme, String) - + var section: ItemListSectionId { switch self { case .selectFromGallery, .selectFromFiles, .scanDocument: @@ -94,7 +104,7 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return AttachmentFileSection.global.rawValue } } - + var stableId: Int32 { switch self { case .selectFromGallery: @@ -105,25 +115,25 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return 2 case .savedHeader: return 3 - case let .savedFile(index, _, _): + case let .savedFile(index, _, _, _): return 4 + index case .savedShowMore: return 9999 case .recentHeader: return 10000 - case let .recentFile(index, _, _): + case let .recentFile(index, _, _, _): return 10001 + index case .recentShowMore: return 100000 case .globalHeader: return 100001 - case let .globalFile(index, _, _): + case let .globalFile(index, _, _, _): return 100002 + index case .globalShowMore: return 200000 } } - + static func ==(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool { switch lhs { case let .selectFromGallery(lhsTheme, lhsText): @@ -150,8 +160,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } else { return false } - case let .savedFile(lhsIndex, lhsTheme, lhsMessage): - if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) { + case let .savedFile(lhsIndex, lhsTheme, lhsMessage, lhsSelection): + if case let .savedFile(rhsIndex, rhsTheme, rhsMessage, rhsSelection) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsSelection == rhsSelection { return true } else { return false @@ -168,8 +178,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } else { return false } - case let .recentFile(lhsIndex, lhsTheme, lhsMessage): - if case let .recentFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) { + case let .recentFile(lhsIndex, lhsTheme, lhsMessage, lhsSelection): + if case let .recentFile(rhsIndex, rhsTheme, rhsMessage, rhsSelection) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsSelection == rhsSelection { return true } else { return false @@ -186,8 +196,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } else { return false } - case let .globalFile(lhsIndex, lhsTheme, lhsMessage): - if case let .globalFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) { + case let .globalFile(lhsIndex, lhsTheme, lhsMessage, lhsSelection): + if case let .globalFile(rhsIndex, rhsTheme, rhsMessage, rhsSelection) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsSelection == rhsSelection { return true } else { return false @@ -200,11 +210,11 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } } } - + static func <(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool { return lhs.stableId < rhs.stableId } - + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! AttachmentFileControllerArguments switch self { @@ -222,56 +232,80 @@ private enum AttachmentFileEntry: ItemListNodeEntry { }) case let .savedHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .savedFile(_, _, message): + case let .savedFile(_, _, message, selection): let interaction = ListMessageItemInteraction(openMessage: { message, _ in - arguments.send(message) + if arguments.isSelectionActive() { + arguments.toggleMessageSelection(message) + } else { + arguments.send(message) + } return false - }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMediaPlayback: { message in + }, openMessageContextMenu: { message, _, node, rect, gesture in + arguments.openMessageContextAction(EngineMessage(message), node, rect, gesture) + }, toggleMediaPlayback: { message in arguments.toggleMediaPlayback(message) - }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) - + }, toggleMessagesSelection: { ids, value in + arguments.setMessageSelection(ids, message, value) + }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) - return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: selection, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) case let .savedShowMore(theme, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandSavedMusic() }) case let .recentHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .recentFile(_, _, message): + case let .recentFile(_, _, message, selection): let interaction = ListMessageItemInteraction(openMessage: { message, _ in - arguments.send(message) + if arguments.isSelectionActive() { + arguments.toggleMessageSelection(message) + } else { + arguments.send(message) + } return false - }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMediaPlayback: { message in + }, openMessageContextMenu: { message, _, node, rect, gesture in + arguments.openMessageContextAction(EngineMessage(message), node, rect, gesture) + }, toggleMediaPlayback: { message in arguments.toggleMediaPlayback(message) - }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) - + }, toggleMessagesSelection: { ids, value in + arguments.setMessageSelection(ids, message, value) + }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) - - return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) + + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: selection, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) case let .recentShowMore(theme, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandRecentMusic() }) case let .globalHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .globalFile(_, _, message): + case let .globalFile(_, _, message, selection): let interaction = ListMessageItemInteraction(openMessage: { message, _ in - arguments.send(message) + if arguments.isSelectionActive() { + arguments.toggleMessageSelection(message) + } else { + arguments.send(message) + } return false - }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMediaPlayback: { message in + }, openMessageContextMenu: { message, _, node, rect, gesture in + arguments.openMessageContextAction(EngineMessage(message), node, rect, gesture) + }, toggleMediaPlayback: { message in arguments.toggleMediaPlayback(message) - }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) - + }, toggleMessagesSelection: { ids, value in + arguments.setMessageSelection(ids, message, value) + }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) - - return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) + + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: selection, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, isAttachMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) case let .globalShowMore(theme, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: { - + }) } } @@ -289,7 +323,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, if hasScan { entries.append(.scanDocument(presentationData.theme, presentationData.strings.Attachment_ScanDocument)) } - + let listTitle: String switch mode { case .recent: @@ -297,21 +331,21 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, case .audio: listTitle = presentationData.strings.Attachment_SharedAudio } - + if case .audio = mode { if let savedMusic, savedMusic.count > 0 { entries.append(.savedHeader(presentationData.theme, presentationData.strings.MediaEditor_Audio_SavedMusic.uppercased())) - + var savedMusic = savedMusic var hasShowMore = false if savedMusic.count > 4 && !state.savedMusicExpanded { savedMusic = Array(savedMusic.prefix(3)) hasShowMore = true } - + var i: Int32 = 0 for file in savedMusic { - entries.append(.savedFile(i, presentationData.theme, file)) + entries.append(.savedFile(i, presentationData.theme, file, messageSelectionState(state: state, message: file))) i += 1 } if hasShowMore { @@ -319,7 +353,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, } } } - + if let recentDocuments { if recentDocuments.count > 0 { entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) @@ -330,10 +364,10 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments = Array(recentDocuments.prefix(3)) hasShowMore = true } - + var i: Int32 = 0 for file in recentDocuments { - entries.append(.recentFile(i, presentationData.theme, file)) + entries.append(.recentFile(i, presentationData.theme, file, messageSelectionState(state: state, message: file))) i += 1 } if hasShowMore { @@ -343,16 +377,47 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, } else { entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) for i in 0 ..< 11 { - entries.append(.recentFile(Int32(i), presentationData.theme, nil)) + entries.append(.recentFile(Int32(i), presentationData.theme, nil, .none)) } } return entries } -private final class AttachmentFileContext: AttachmentMediaPickerContext { +final class AttachmentFileContext: AttachmentMediaPickerContext { + private weak var controller: AttachmentFileControllerImpl? + + var selectionCount: Signal { + if let controller = self.controller { + return controller.selectionCount.get() + } else { + return .single(0) + } + } + + init(controller: AttachmentFileControllerImpl) { + self.controller = controller + } + + func setCaption(_ caption: NSAttributedString) { + self.controller?.caption = caption + } + + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.controller?.mulitpleCompletion?(mode, .files, parameters) + } + + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { +// self.controller?.presentScheduleTimePicker ({ time, repeatPeriod in +// self.controller?.contactsNode.requestMultipleAction?(false, time, parameters) +// }) + } + + func mainButtonAction() { + } } + public class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -365,39 +430,44 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var isMinimized: Bool = false + + fileprivate var caption: NSAttributedString? + fileprivate var selectionCount = ValuePromise(0) + fileprivate var mulitpleCompletion: ((AttachmentMediaPickerSendMode, AttachmentMediaPickerAttachmentMode, ChatSendMessageActionSheetController.SendParameters?) -> Void)? + var delayDisappear = false - + var hasBottomEdgeEffect = true - + var resetForReuseImpl: () -> Void = {} public func resetForReuse() { self.resetForReuseImpl() self.scrollToTop?() } - + public func prepareForReuse() { self.delayDisappear = true self.visibleBottomContentOffsetChanged?(self.visibleBottomContentOffset) self.delayDisappear = false } - + public var mediaPickerContext: AttachmentMediaPickerContext? { - return AttachmentFileContext() + return AttachmentFileContext(controller: self) } - + private var topEdgeEffectView: EdgeEffectView? private var bottomEdgeEffectView: EdgeEffectView? - + var isSearching: Bool = false { didSet { self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) } } - + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - + let topEdgeEffectView: EdgeEffectView if let current = self.topEdgeEffectView { topEdgeEffectView = current @@ -408,12 +478,12 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon } self.topEdgeEffectView = topEdgeEffectView } - + let edgeEffectHeight: CGFloat = 88.0 let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame) topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) - + if self.hasBottomEdgeEffect { let bottomEdgeEffectView: EdgeEffectView if let current = self.bottomEdgeEffectView { @@ -423,7 +493,7 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon self.view.addSubview(bottomEdgeEffectView) self.bottomEdgeEffectView = bottomEdgeEffectView } - + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame) transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0) @@ -438,6 +508,15 @@ private struct AttachmentFileControllerState: Equatable { var searching: Bool var savedMusicExpanded: Bool var recentMusicExpanded: Bool + var selectedMessageIds: Set? + var messageMap: [MessageId: EngineMessage] +} + +private func messageSelectionState(state: AttachmentFileControllerState, message: Message?) -> ChatHistoryMessageSelection { + guard let message = message, let selectedMessageIds = state.selectedMessageIds else { + return .none + } + return .selectable(selected: selectedMessageIds.contains(message.id)) } public enum AttachmentFileControllerMode { @@ -453,21 +532,24 @@ public func makeAttachmentFileControllerImpl( presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, - send: @escaping (AnyMediaReference) -> Void + send: @escaping ([AnyMediaReference]) -> Void ) -> AttachmentFileController { let actionsDisposable = DisposableSet() - - let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false, recentMusicExpanded: false), ignoreRepeated: true) - let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false, recentMusicExpanded: false)) + + let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false, recentMusicExpanded: false, selectedMessageIds: nil, messageMap: [:]), ignoreRepeated: true) + let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false, recentMusicExpanded: false, selectedMessageIds: nil, messageMap: [:])) let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - + var updateTabBarVisibilityImpl: ((Bool) -> Void)? var expandImpl: (() -> Void)? var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var updateIsSearchingImpl: ((Bool) -> Void)? + var updateSelectionCountImpl: ((Int) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + let arguments = AttachmentFileControllerArguments( context: context, isAudio: mode == .audio, @@ -498,7 +580,7 @@ public func makeAttachmentFileControllerImpl( send: { message in if message.id.namespace == Namespaces.Message.Local { if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { - send(.standalone(media: file)) + send([.standalone(media: file)]) dismissImpl?() } } else { @@ -514,7 +596,7 @@ public func makeAttachmentFileControllerImpl( } |> deliverOnMainQueue).startStandalone(next: { messages in if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { - send(.message(message: MessageReference(message), media: file)) + send([.message(message: MessageReference(message), media: file)]) } dismissImpl?() }) @@ -523,9 +605,83 @@ public func makeAttachmentFileControllerImpl( toggleMediaPlayback: { message in let playlistLocation: PeerMessagesPlaylistLocation = .custom(messages: .single(([message], 0, false)), canReorder: false, at: message.id, loadMore: nil) context.sharedContext.mediaManager.setPlaylist((context, PeerMessagesMediaPlaylist(context: context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.togglePlayPause)) + }, + isSelectionActive: { + return stateValue.with { $0.selectedMessageIds != nil } + }, + toggleMessageSelection: { message in + updateState { state in + guard var selectedMessageIds = state.selectedMessageIds else { + return state + } + let messageId = message.id + if selectedMessageIds.contains(messageId) { + selectedMessageIds.remove(messageId) + } else { + selectedMessageIds.insert(messageId) + } + var updatedState = state + updatedState.selectedMessageIds = selectedMessageIds + updatedState.messageMap[messageId] = EngineMessage(message) + updateSelectionCountImpl?(selectedMessageIds.count) + return updatedState + } + }, + setMessageSelection: { messageIds, message, value in + updateState { state in + guard var selectedMessageIds = state.selectedMessageIds else { + return state + } + for messageId in messageIds { + if value { + selectedMessageIds.insert(messageId) + } else { + selectedMessageIds.remove(messageId) + } + } + var updatedState = state + updatedState.selectedMessageIds = selectedMessageIds + if let message { + updatedState.messageMap[message.id] = EngineMessage(message) + } + updateSelectionCountImpl?(selectedMessageIds.count) + return updatedState + } + }, + openMessageContextAction: { message, node, rect, anyRecognizer in + guard let node = node as? ContextExtractedContentContainingNode else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let gesture: ContextGesture? = anyRecognizer as? ContextGesture + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.KeyCommand_Play, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Play"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + c?.dismiss(completion: {}) + + let playlistLocation: PeerMessagesPlaylistLocation = .custom(messages: .single(([message._asMessage()], 0, false)), canReorder: false, at: message.id, loadMore: nil) + context.sharedContext.mediaManager.setPlaylist((context, PeerMessagesMediaPlaylist(context: context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.togglePlayPause)) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + c?.dismiss(completion: {}) + updateState { state in + var updatedState = state + var selectedMessageIds = updatedState.selectedMessageIds ?? Set() + selectedMessageIds.insert(message.id) + updatedState.selectedMessageIds = selectedMessageIds + updatedState.messageMap[message.id] = message + updateSelectionCountImpl?(selectedMessageIds.count) + return updatedState + } + }))) + + let controller = makeContextController(presentationData: presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + presentInGlobalOverlayImpl?(controller) } ) - + let recentDocuments: Signal<[Message]?, NoError> let savedMusicContext: ProfileSavedMusicContext? let savedMusic: Signal<[Message]?, NoError> @@ -564,12 +720,12 @@ public func makeAttachmentFileControllerImpl( } ) } - + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - + let existingCloseButton = Atomic(value: nil) let existingSearchButton = Atomic(value: nil) - + let previousRecentDocuments = Atomic<[Message]?>(value: nil) let signal = combineLatest(queue: Queue.mainQueue(), presentationData, @@ -579,10 +735,10 @@ public func makeAttachmentFileControllerImpl( ) |> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in var presentationData = presentationData - + let updatedTheme = presentationData.theme.withModalBlocksBackground() presentationData = presentationData.withUpdated(theme: updatedTheme) - + let barButtonSize = CGSize(width: 44.0, height: 44.0) let closeButton = GlassBarButtonComponent( size: barButtonSize, @@ -611,7 +767,7 @@ public func makeAttachmentFileControllerImpl( } return buttonNode } - + let searchButton = GlassBarButtonComponent( size: barButtonSize, backgroundColor: nil, @@ -634,8 +790,9 @@ public func makeAttachmentFileControllerImpl( updateIsSearchingImpl?(true) } ) - let searchButtonComponent = state.searching ? nil : AnyComponentWithIdentity(id: "search", component: AnyComponent(searchButton)) - let searchButtonNode: BarComponentHostNode? = !state.searching ? existingSearchButton.modify { current in + let isSelectionActive = state.selectedMessageIds != nil + let searchButtonComponent = (state.searching || isSelectionActive) ? nil : AnyComponentWithIdentity(id: "search", component: AnyComponent(searchButton)) + let searchButtonNode: BarComponentHostNode? = (!state.searching && !isSelectionActive) ? existingSearchButton.modify { current in let buttonNode: BarComponentHostNode if let current { buttonNode = current @@ -645,7 +802,7 @@ public func makeAttachmentFileControllerImpl( } return buttonNode } : nil - + let previousRecentDocuments = previousRecentDocuments.swap(recentDocuments) let crossfade = previousRecentDocuments == nil && recentDocuments != nil var animateChanges = false @@ -655,19 +812,19 @@ public func makeAttachmentFileControllerImpl( !crossfade { animateChanges = true } - + let leftNavigationButton = closeButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) } - + var hasAudioSearch = false if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["music_search_username"] as? String { hasAudioSearch = true } - + var rightNavigationButton: ItemListNavigationButton? if bannedSendMedia == nil && (recentDocuments == nil || (recentDocuments?.count ?? 0) > 10 || (mode == .audio && hasAudioSearch)) { rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) } } - + let title: String switch mode { case .recent: @@ -675,7 +832,7 @@ public func makeAttachmentFileControllerImpl( case .audio: title = presentationData.strings.MediaEditor_Audio_Title } - + let controllerState = ItemListControllerState( presentationData: ItemListPresentationData(presentationData), title: .text(title), @@ -684,7 +841,7 @@ public func makeAttachmentFileControllerImpl( backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true ) - + var emptyItem: AttachmentFileEmptyStateItem? if let (untilDate, personal) = bannedSendMedia { let banDescription: String @@ -700,7 +857,7 @@ public func makeAttachmentFileControllerImpl( recentDocuments.isEmpty { emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .intro) } - + var searchItem: ItemListControllerSearch? if state.searching { searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: { @@ -719,17 +876,32 @@ public func makeAttachmentFileControllerImpl( dismissInputImpl?() }) } - + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, hasScan: presentDocumentScanner != nil, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) - + return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() let _ = savedMusicContext?.state } - + let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true) + controller.mulitpleCompletion = { _, _, _ in + let _ = stateValue.with({ state in + if let selectedMessageIds = state.selectedMessageIds { + var mediaReferences: [AnyMediaReference] = [] + for id in selectedMessageIds { + if let message = state.messageMap[id]?._asMessage(), let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile { + mediaReferences.append(.standalone(media: file)) + } + } + send(mediaReferences) + dismissImpl?() + } + }) + + } controller.delayDisappear = true controller.visibleBottomContentOffsetChanged = { [weak controller] offset in switch offset { @@ -751,6 +923,7 @@ public func makeAttachmentFileControllerImpl( updateState { state in var updatedState = state updatedState.searching = false + updatedState.selectedMessageIds = nil return updatedState } } @@ -769,6 +942,12 @@ public func makeAttachmentFileControllerImpl( updateTabBarVisibilityImpl = { [weak controller] isVisible in controller?.updateTabBarVisibility(isVisible, .animated(duration: 0.4, curve: .spring)) } + updateSelectionCountImpl = { [weak controller] count in + controller?.selectionCount.set(count) + } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c) + } return controller } @@ -786,8 +965,8 @@ public func storyAudioPickerController( let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: { selectFromFiles() dismissImpl?() - }, presentDocumentScanner: nil, send: { file in - completion(file) + }, presentDocumentScanner: nil, send: { files in + completion(files.first!) dismissImpl?() }) as! AttachmentFileControllerImpl present(filePickerController, filePickerController.mediaPickerContext) @@ -803,3 +982,26 @@ public func storyAudioPickerController( } return controller } + +private final class MessageContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + + let shouldBeDismissed: Signal + + private let sourceNode: ContextExtractedContentContainingNode + + init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal? = nil) { + self.sourceNode = sourceNode + self.shouldBeDismissed = shouldBeDismissed ?? .single(false) + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index 01bc1ddc4f..c0711a7afe 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -216,11 +216,13 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { private final class AttachmentFileSearchContainerInteraction { let context: AccountContext let send: (Message) -> Void + let toggleMediaPlayback: (Message) -> Void let expandSection: (Int32) -> Void - init(context: AccountContext, send: @escaping (Message) -> Void, expandSection: @escaping (Int32) -> Void) { + init(context: AccountContext, send: @escaping (Message) -> Void, toggleMediaPlayback: @escaping (Message) -> Void, expandSection: @escaping (Int32) -> Void) { self.context = context self.send = send + self.toggleMediaPlayback = toggleMediaPlayback self.expandSection = expandSection } } @@ -322,13 +324,15 @@ private enum AttachmentFileSearchEntry: Comparable, Identifiable { let itemInteraction = ListMessageItemInteraction(openMessage: { message, _ in interaction.send(message) return false - }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMediaPlayback: { _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMediaPlayback: { message in + interaction.toggleMediaPlayback(message) + }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) let displayFileInfo = mode == .audio let isStoryMusic = mode == .audio let isDownloadList = mode == .audio - return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: false, isDownloadList: isDownloadList, isStoryMusic: isStoryMusic, displayFileInfo: displayFileInfo, displayBackground: true, style: .blocks, sectionId: section) + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: false, isDownloadList: isDownloadList, isStoryMusic: isStoryMusic, isAttachMusic: true, displayFileInfo: displayFileInfo, displayBackground: true, style: .blocks, sectionId: section) case let .showMore(text, section): return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), style: .blocks, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: section, editing: false, action: { interaction.expandSection(section) @@ -459,10 +463,17 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon self.addSubnode(self.emptyResultsTextNode) - let interaction = AttachmentFileSearchContainerInteraction(context: context, send: { [weak self] message in - send(message) - self?.listNode.clearHighlightAnimated(true) - }, expandSection: { [weak self] section in + let interaction = AttachmentFileSearchContainerInteraction( + context: context, + send: { [weak self] message in + send(message) + self?.listNode.clearHighlightAnimated(true) + }, + toggleMediaPlayback: { message in + let playlistLocation: PeerMessagesPlaylistLocation = .custom(messages: .single(([message], 0, false)), canReorder: false, at: message.id, loadMore: nil) + context.sharedContext.mediaManager.setPlaylist((context, PeerMessagesMediaPlaylist(context: context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.togglePlayPause)) + }, + expandSection: { [weak self] section in self?.expandedSections.insert(section) }) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index dc28153fa1..34d0d2d31f 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -845,7 +845,7 @@ final class ComposePollScreenComponent: Component { var availableButtons: [AttachmentButtonType] switch subject { - case .description: + case .description, .quizAnswer: availableButtons = [.gallery, .file, .location] default: availableButtons = [.gallery, .sticker, .emoji, .location] diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift index cea20f6258..b4be8247fe 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift @@ -79,8 +79,8 @@ public func presentPollAttachmentScreen( //self?.presentICloudFileGallery() }, presentDocumentScanner: { //self?.presentDocumentScanner() - }, send: { mediaReference in - completion(mediaReference) + }, send: { mediaReferences in + completion(mediaReferences.first!) }) guard let controller = controller as? AttachmentFileControllerImpl else { return false diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 4fac2c7f26..0b7f591261 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1948,7 +1948,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } attachmentController?.dismiss(animated: true) self.presentICloudFileGallery(view: view, peer: peer, replyMessageId: nil, replyToStoryId: focusedStoryId) - }, presentDocumentScanner: nil, send: { [weak view] mediaReference in + }, presentDocumentScanner: nil, send: { [weak view] mediaReferences in guard let view, let component = view.component else { return } @@ -1956,7 +1956,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) } - let message: EnqueueMessage = .message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: mediaReference, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: mediaReferences.first!, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(nil)]) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in if let self, let view { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/Contents.json new file mode 100644 index 0000000000..5070444c10 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "emojitab.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/emojitab.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/emojitab.pdf new file mode 100644 index 0000000000..b17db60926 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Emoji.imageset/emojitab.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/Contents.json new file mode 100644 index 0000000000..6c6efa3dbd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stickertab.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/stickertab.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/stickertab.pdf new file mode 100644 index 0000000000..6f15208ad6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Sticker.imageset/stickertab.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index ea763248aa..b3bc0ba70a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -381,13 +381,20 @@ extension ChatControllerImpl { self?.presentICloudFileGallery() }, presentDocumentScanner: { [weak self] in self?.presentDocumentScanner() - }, send: { [weak self] mediaReference in + }, send: { [weak self] mediaReferences in guard let self else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + var messages: [EnqueueMessage] = [] + var groupingKey: Int64? + if mediaReferences.count > 1 { + groupingKey = Int64.random(in: .min ..< .max) + } + for mediaReference in mediaReferences { + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in - self?.sendMessages([message], media: true, postpone: postpone) + self?.sendMessages(messages, media: true, postpone: postpone) }) }) if let controller = controller as? AttachmentFileControllerImpl { @@ -407,13 +414,20 @@ extension ChatControllerImpl { }, presentFiles: { [weak self, weak attachmentController] in attachmentController?.dismiss(animated: true) self?.presentICloudFileGallery() - }, presentDocumentScanner: nil, send: { [weak self] mediaReference in + }, presentDocumentScanner: nil, send: { [weak self] mediaReferences in guard let self else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + var messages: [EnqueueMessage] = [] + var groupingKey: Int64? + if mediaReferences.count > 1 { + groupingKey = Int64.random(in: .min ..< .max) + } + for mediaReference in mediaReferences { + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in - self?.sendMessages([message], media: true, postpone: postpone) + self?.sendMessages(messages, media: true, postpone: postpone) }) }) if let controller = controller as? AttachmentFileControllerImpl { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3a666067ed..65eb67fb46 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2733,7 +2733,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { + public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference]) -> Void) -> AttachmentFileController { return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: audio ? .audio : .recent, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, presentDocumentScanner: presentDocumentScanner, send: send) }