diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7071f7a3b2..1e3eadbe4b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -466,8 +466,9 @@ public final class NavigateToChatControllerParams { public let changeColors: Bool public let setupController: (ChatController) -> Void public let completion: (ChatController) -> Void + public let pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -495,6 +496,7 @@ public final class NavigateToChatControllerParams { self.chatNavigationStack = chatNavigationStack self.changeColors = changeColors self.setupController = setupController + self.pushController = pushController self.completion = completion } } diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index a4a430170f..42a0601204 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -23,6 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject { func setSoundEnabled(_ value: Bool) func seek(_ timestamp: Double) func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) + func continueWithOverridingAmbientMode() func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) @@ -283,6 +284,14 @@ public final class UniversalVideoNode: ASDisplayNode { }) } + public func continueWithOverridingAmbientMode() { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.continueWithOverridingAmbientMode() + } + }) + } + public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index a255ad726a..a015d5cd04 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -97,6 +97,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryContentComponent", "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", "//submodules/TelegramUI/Components/FullScreenEffectView", + "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index dfecee9570..6095487d7a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -177,6 +177,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private(set) var storySubscriptions: EngineStorySubscriptions? + private var storyProgressDisposable: Disposable? private var storySubscriptionsDisposable: Disposable? private var preloadStorySubscriptionsDisposable: Disposable? private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:] @@ -717,6 +718,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.powerSavingMonitoringDisposable?.dispose() self.storySubscriptionsDisposable?.dispose() self.preloadStorySubscriptionsDisposable?.dispose() + self.storyProgressDisposable?.dispose() } private func updateNavigationMetadata() { @@ -1240,7 +1242,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - self.chatListDisplayNode.mainContainerNode.openStories = { [weak self] peerId in + self.chatListDisplayNode.mainContainerNode.openStories = { [weak self] peerId, itemNode in guard let self else { return } @@ -1249,16 +1251,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = (storyContent.state |> filter { $0.slice != nil } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in guard let self else { return } + var transitionIn: StoryContainerScreen.TransitionIn? + if let itemNode = itemNode as? ChatListItemNode { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: itemNode.avatarNode.view, + sourceRect: itemNode.avatarNode.view.bounds, + sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5, + sourceIsAvatar: true + ) + itemNode.avatarNode.isHidden = true + } + let storyContainerScreen = StoryContainerScreen( context: self.context, content: storyContent, - transitionIn: nil, + transitionIn: transitionIn, transitionOut: { _, _ in + if let itemNode = itemNode as? ChatListItemNode { + let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view) + return StoryContainerScreen.TransitionOut( + destinationView: itemNode.view, + transitionView: nil, + destinationRect: rect, + destinationCornerRadius: rect.height * 0.5, + destinationIsAvatar: true, + completed: { [weak itemNode] in + guard let itemNode else { + return + } + itemNode.avatarNode.isHidden = false + } + ) + } return nil } ) @@ -1760,22 +1789,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in var chatListState = chatListState - var peersWithNewStories = Set() + var peerStoryMapping: [EnginePeer.Id: Bool] = [:] for item in storySubscriptions.items { if item.peer.id == self.context.account.peerId { continue } - if item.hasUnseen { - peersWithNewStories.insert(item.peer.id) - } + peerStoryMapping[item.peer.id] = item.hasUnseen } - chatListState.peersWithNewStories = peersWithNewStories + chatListState.peerStoryMapping = peerStoryMapping return chatListState } self.storiesReady.set(.single(true)) }) + self.storyProgressDisposable = (self.context.engine.messages.allStoriesUploadProgress() + |> deliverOnMainQueue).start(next: { [weak self] progress in + guard let self else { + return + } + self.updateStoryUploadProgress(progress) + }) } } @@ -2345,7 +2379,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 + sourceCornerRadius: transitionView.bounds.height * 0.5, + sourceIsAvatar: true ) } } @@ -2506,9 +2541,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private(set) var storyUploadProgress: Float? - public func updateStoryUploadProgress(_ progress: Float?) { + private func updateStoryUploadProgress(_ progress: Float?) { self.storyUploadProgress = progress.flatMap { max(0.027, min(0.99, $0)) } - self.chatListDisplayNode.requestNavigationBarLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) + + if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarView.updateStoryUploadProgress(storyUploadProgress: self.storyUploadProgress) + } } public func scrollToStories() { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index da8ffefc24..88757ac26f 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -191,7 +191,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _ in }) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in @@ -240,7 +240,7 @@ private final class ChatListShimmerNode: ASDisplayNode { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } @@ -979,8 +979,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele itemNode.listNode.activateChatPreview = { [weak self] item, threadId, sourceNode, gesture, location in self?.activateChatPreview?(item, threadId, sourceNode, gesture, location) } - itemNode.listNode.openStories = { [weak self] peerId in - self?.openStories?(peerId) + itemNode.listNode.openStories = { [weak self] peerId, itemNode in + self?.openStories?(peerId, itemNode) } itemNode.listNode.addedVisibleChatsWithPeerIds = { [weak self] ids in self?.addedVisibleChatsWithPeerIds?(ids) @@ -1052,7 +1052,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - var openStories: ((EnginePeer.Id) -> Void)? + var openStories: ((EnginePeer.Id, ASDisplayNode?) -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? var canExpandHiddenItems: (() -> Bool)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index d56cc396f5..158bcbd102 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): @@ -2167,7 +2167,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPremiumIntro: { }, openChatFolderUpdates: { }, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) chatListInteraction.isSearchMode = true @@ -3402,7 +3402,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) var isInlineMode = false if case .topics = key { @@ -3458,7 +3458,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) case .media: return nil diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 769f7b45e3..ac741a0dc0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -26,6 +26,7 @@ import TextNodeWithEntities import ComponentFlow import EmojiStatusComponent import AvatarVideoNode +import AvatarStoryIndicatorComponent public enum ChatListItemContent { public struct ThreadInfo: Equatable { @@ -82,7 +83,7 @@ public enum ChatListItemContent { public var forumTopicData: EngineChatList.ForumTopicData? public var topForumTopicItems: [EngineChatList.ForumTopicData] public var autoremoveTimeout: Int32? - public var hasNewStories: Bool + public var storyState: Bool? public init( messages: [EngineMessage], @@ -102,7 +103,7 @@ public enum ChatListItemContent { forumTopicData: EngineChatList.ForumTopicData?, topForumTopicItems: [EngineChatList.ForumTopicData], autoremoveTimeout: Int32?, - hasNewStories: Bool + storyState: Bool? ) { self.messages = messages self.peer = peer @@ -121,7 +122,7 @@ public enum ChatListItemContent { self.forumTopicData = forumTopicData self.topForumTopicItems = topForumTopicItems self.autoremoveTimeout = autoremoveTimeout - self.hasNewStories = hasNewStories + self.storyState = storyState } } @@ -902,7 +903,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarIconView: ComponentHostView? var avatarIconComponent: EmojiStatusComponent? var avatarVideoNode: AvatarVideoNode? - var avatarStoryIndicatorNode: ASImageNode? + var avatarStoryIndicator: ComponentView? private var inlineNavigationMarkLayer: SimpleLayer? @@ -2746,9 +2747,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0) - var displayStoryIndicator = false - if case let .peer(peerData) = item.content, peerData.hasNewStories { - displayStoryIndicator = true + var displayStoryIndicator: Bool? + if case let .peer(peerData) = item.content { + displayStoryIndicator = peerData.storyState } let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) @@ -2763,7 +2764,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let storyIndicatorScale = avatarScale - if displayStoryIndicator { + if displayStoryIndicator != nil { avatarScale *= (avatarFrame.width - 4.0 * 2.0) / avatarFrame.width } @@ -2774,50 +2775,47 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.avatarNode.updateSize(size: avatarFrame.size) strongSelf.updateVideoVisibility() - if displayStoryIndicator { - let avatarStoryIndicatorNode: ASImageNode - if let current = strongSelf.avatarStoryIndicatorNode { - avatarStoryIndicatorNode = current + if let displayStoryIndicator { + var indicatorTransition = Transition(transition) + let avatarStoryIndicator: ComponentView + if let current = strongSelf.avatarStoryIndicator { + avatarStoryIndicator = current } else { - avatarStoryIndicatorNode = ASImageNode() - strongSelf.avatarStoryIndicatorNode = avatarStoryIndicatorNode - strongSelf.contextContainer.insertSubnode(avatarStoryIndicatorNode, belowSubnode: strongSelf.avatarContainerNode) - - avatarStoryIndicatorNode.isUserInteractionEnabled = true - avatarStoryIndicatorNode.view.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:)))) + indicatorTransition = .immediate + avatarStoryIndicator = ComponentView() + strongSelf.avatarStoryIndicator = avatarStoryIndicator } - var updateImage = false - if let image = avatarStoryIndicatorNode.image { - if image.size != avatarFrame.size { - updateImage = true + + var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 4.0, y: avatarFrame.minY + 4.0), size: CGSize(width: avatarFrame.width - 4.0 - 4.0, height: avatarFrame.height - 4.0 - 4.0)) + indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5 + + let _ = avatarStoryIndicator.update( + transition: indicatorTransition, + component: AnyComponent(AvatarStoryIndicatorComponent( + hasUnseen: displayStoryIndicator, + isDarkTheme: item.presentationData.theme.overallDarkAppearance, + activeLineWidth: 2.0, + inactiveLineWidth: 1.0 + UIScreenPixel + )), + environment: {}, + containerSize: indicatorFrame.size + ) + if let avatarStoryIndicatorView = avatarStoryIndicator.view { + if avatarStoryIndicatorView.superview == nil { + avatarStoryIndicatorView.isUserInteractionEnabled = true + avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:)))) + + strongSelf.contextContainer.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarContainerNode.view) } - } else { - updateImage = true + + indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center) + indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) + indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale) } - if updateImage { - avatarStoryIndicatorNode.image = generateImage(avatarFrame.size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - let lineWidth: CGFloat = 2.0 - context.setLineWidth(lineWidth) - context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) - context.replacePathWithStrokedPath() - context.clip() - - var locations: [CGFloat] = [1.0, 0.0] - let colors: [CGColor] = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) - } - transition.updateFrame(node: avatarStoryIndicatorNode, frame: CGRect(origin: CGPoint(x: avatarFrame.minX, y: avatarFrame.minY + (avatarFrame.height - avatarFrame.height * storyIndicatorScale) * 0.5), size: CGSize(width: avatarFrame.width * storyIndicatorScale, height: avatarFrame.height * storyIndicatorScale))) } else { - if let avatarStoryIndicatorNode = strongSelf.avatarStoryIndicatorNode { - strongSelf.avatarStoryIndicatorNode = nil - avatarStoryIndicatorNode.removeFromSupernode() + if let avatarStoryIndicator = strongSelf.avatarStoryIndicator { + strongSelf.avatarStoryIndicator = nil + avatarStoryIndicator.view?.removeFromSuperview() } } @@ -3844,7 +3842,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if let avatarStoryIndicatorNode = self.avatarStoryIndicatorNode, let result = avatarStoryIndicatorNode.view.hitTest(self.view.convert(point, to: avatarStoryIndicatorNode.view), with: event) { + if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) { return result } @@ -3856,7 +3854,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { guard let item = self.item, case let .peer(peerData) = item.content else { return } - item.interaction.openStories(peerData.peer.peerId) + item.interaction.openStories(peerData.peer.peerId, self) } } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fe5bf95e5b..fedf0f252d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -100,7 +100,7 @@ public final class ChatListNodeInteraction { let openPremiumIntro: () -> Void let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void - let openStories: (EnginePeer.Id) -> Void + let openStories: (EnginePeer.Id, ASDisplayNode?) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -148,7 +148,7 @@ public final class ChatListNodeInteraction { openPremiumIntro: @escaping () -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, - openStories: @escaping (EnginePeer.Id) -> Void + openStories: @escaping (EnginePeer.Id, ASDisplayNode?) -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -241,7 +241,7 @@ public struct ChatListNodeState: Equatable { public var foundPeers: [(EnginePeer, EnginePeer?)] public var selectedPeerMap: [EnginePeer.Id: EnginePeer] public var selectedThreadIds: Set - public var peersWithNewStories: Set + public var peerStoryMapping: [EnginePeer.Id: Bool] public init( presentationData: ChatListPresentationData, @@ -257,7 +257,7 @@ public struct ChatListNodeState: Equatable { hiddenItemShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: EnginePeer.Id?, selectedThreadIds: Set, - peersWithNewStories: Set + peerStoryMapping: [EnginePeer.Id: Bool] ) { self.presentationData = presentationData self.editing = editing @@ -272,7 +272,7 @@ public struct ChatListNodeState: Equatable { self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed self.hiddenPsaPeerId = hiddenPsaPeerId self.selectedThreadIds = selectedThreadIds - self.peersWithNewStories = peersWithNewStories + self.peerStoryMapping = peerStoryMapping } public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { @@ -315,7 +315,7 @@ public struct ChatListNodeState: Equatable { if lhs.selectedThreadIds != rhs.selectedThreadIds { return false } - if lhs.peersWithNewStories != rhs.peersWithNewStories { + if lhs.peerStoryMapping != rhs.peerStoryMapping { return false } return true @@ -396,7 +396,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL forumTopicData: forumTopicData, topForumTopicItems: topForumTopicItems, autoremoveTimeout: peerEntry.autoremoveTimeout, - hasNewStories: peerEntry.hasNewStories + storyState: peerEntry.storyState )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -742,7 +742,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL forumTopicData: forumTopicData, topForumTopicItems: topForumTopicItems, autoremoveTimeout: peerEntry.autoremoveTimeout, - hasNewStories: peerEntry.hasNewStories + storyState: peerEntry.storyState )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -1086,7 +1086,7 @@ public final class ChatListNode: ListView { public var toggleArchivedFolderHiddenByDefault: (() -> Void)? public var hidePsa: ((EnginePeer.Id) -> Void)? public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - public var openStories: ((EnginePeer.Id) -> Void)? + public var openStories: ((EnginePeer.Id, ASDisplayNode?) -> Void)? private var theme: PresentationTheme @@ -1210,7 +1210,7 @@ public final class ChatListNode: ListView { isSelecting = true } - self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), peersWithNewStories: Set()) + self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), peerStoryMapping: [:]) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme @@ -1549,11 +1549,11 @@ public final class ChatListNode: ListView { let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start() } }) - }, openStories: { [weak self] peerId in + }, openStories: { [weak self] peerId, itemNode in guard let self else { return } - self.openStories?(peerId) + self.openStories?(peerId, itemNode) }) nodeInteraction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 53350b7ace..1d90f7a111 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -111,7 +111,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { var forumTopicData: EngineChatList.ForumTopicData? var topForumTopicItems: [EngineChatList.ForumTopicData] var revealed: Bool - var hasNewStories: Bool + var storyState: Bool? init( index: EngineChatList.Item.Index, @@ -136,7 +136,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { forumTopicData: EngineChatList.ForumTopicData?, topForumTopicItems: [EngineChatList.ForumTopicData], revealed: Bool, - hasNewStories: Bool + storyState: Bool? ) { self.index = index self.presentationData = presentationData @@ -160,7 +160,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { self.forumTopicData = forumTopicData self.topForumTopicItems = topForumTopicItems self.revealed = revealed - self.hasNewStories = hasNewStories + self.storyState = storyState } static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool { @@ -270,7 +270,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhs.revealed != rhs.revealed { return false } - if lhs.hasNewStories != rhs.hasNewStories { + if lhs.storyState != rhs.storyState { return false } return true @@ -639,7 +639,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState forumTopicData: entry.forumTopicData, topForumTopicItems: entry.topForumTopicItems, revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing), - hasNewStories: state.peersWithNewStories.contains(entry.renderedPeer.peerId) + storyState: state.peerStoryMapping[entry.renderedPeer.peerId] )) if let threadInfo, threadInfo.isHidden { @@ -689,7 +689,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState forumTopicData: nil, topForumTopicItems: [], revealed: false, - hasNewStories: false + storyState: nil ))) if foundPinningIndex != 0 { foundPinningIndex -= 1 @@ -720,7 +720,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState forumTopicData: nil, topForumTopicItems: [], revealed: false, - hasNewStories: false + storyState: nil ))) } else { if !filteredAdditionalItemEntries.isEmpty { @@ -771,7 +771,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState forumTopicData: item.item.forumTopicData, topForumTopicItems: item.item.topForumTopicItems, revealed: state.hiddenItemShouldBeTemporaryRevealed || state.editing, - hasNewStories: false + storyState: nil ))) if pinningIndex != 0 { pinningIndex -= 1 diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 16ad97877a..1518c0b3e2 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -533,7 +533,8 @@ public class ContactsController: ViewController { transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 + sourceCornerRadius: transitionView.bounds.height * 0.5, + sourceIsAvatar: true ) } } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 11eff8d81b..0129bd72a6 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -254,23 +254,6 @@ final class ContactsControllerNode: ASDisplayNode { self.controller?.requestLayout(transition: transition) //self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil - /*self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in - var chatListState = chatListState - - var peersWithNewStories = Set() - for item in storySubscriptions.items { - if item.peer.id == self.context.account.peerId { - continue - } - if item.hasUnseen { - peersWithNewStories.insert(item.peer.id) - } - } - chatListState.peersWithNewStories = peersWithNewStories - - return chatListState - }*/ - self.storiesReady.set(.single(true)) }) } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index aeca6ea9e9..1da6531b2e 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -95,7 +95,7 @@ public final class HashtagSearchController: TelegramBaseController { }, openPremiumIntro: { }, openChatFolderUpdates: { }, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 9c81f7975c..65329922cd 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -599,6 +599,29 @@ private final class MediaPlayerContext { self.stoppedAtEnd = false } + fileprivate func continueWithOverridingAmbientMode() { + if self.ambient { + self.ambient = false + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case .seeking: + break + } + + if let loadedState = loadedState { + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) + } + } + } + fileprivate func continuePlayingWithoutSound() { if self.enableSound { self.lastStatusUpdateTimestamp = nil @@ -1134,6 +1157,14 @@ public final class MediaPlayer { } } + public func continueWithOverridingAmbientMode() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continueWithOverridingAmbientMode() + } + } + } + public func continuePlayingWithoutSound() { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 2922a37a5c..20858e54fe 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -341,6 +341,17 @@ public final class MediaBox { return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)" } + public func cachedRepresentationCompletePath(_ id: MediaResourceId, keepDuration: CachedMediaRepresentationKeepDuration, representationId: String) -> String { + let cacheString: String + switch keepDuration { + case .general: + cacheString = "cache" + case .shortLived: + cacheString = "short-cache" + } + return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representationId)" + } + public func shortLivedResourceCachePathPrefix(_ id: MediaResourceId) -> String { let cacheString = "short-cache" return "\(self.basePath)/\(cacheString)/\(fileNameForId(id))" diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 2beea7579e..d2ffb161b3 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -223,7 +223,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) @@ -290,7 +290,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index ff1437b1d4..6b4d9af2bb 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -857,7 +857,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) @@ -923,7 +923,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 2495ab4d75..e48ad2ef67 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) func makeChatListItem( @@ -436,7 +436,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 83c6ec8e9c..9785236e65 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -25,10 +25,10 @@ public enum ManagedAudioSessionType: Equatable { var isPlay: Bool { switch self { - case .play, .playWithPossiblePortOverride: - return true - default: - return false + case .play, .ambient, .playWithPossiblePortOverride: + return true + default: + return false } } } @@ -186,7 +186,7 @@ public class ManagedAudioSessionControl { } } -public final class ManagedAudioSession { +public final class ManagedAudioSession: NSObject { public private(set) static var shared: ManagedAudioSession? private var nextId: Int32 = 0 @@ -211,6 +211,11 @@ public final class ManagedAudioSession { private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>() + private let volumeUpDetectedPromise = Promise() + public var volumeUpDetected: Signal { + return self.volumeUpDetectedPromise.get() + } + private var availableOutputsValue: [AudioSessionOutput] = [] private var currentOutputValue: AudioSessionOutput? @@ -220,11 +225,13 @@ public final class ManagedAudioSession { private var isActiveValue: Bool = false private var callKitAudioSessionIsActive: Bool = false - public init() { + override public init() { self.queue = Queue() self.hasLoudspeaker = UIDevice.current.model == "iPhone" + super.init() + let queue = self.queue NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in queue.async { @@ -263,6 +270,8 @@ public final class ManagedAudioSession { }) }) + AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: [.new, .old], context: nil) + queue.async { self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn() self.updateCurrentAudioRouteInfo() @@ -273,6 +282,17 @@ public final class ManagedAudioSession { deinit { self.deactivateTimer?.invalidate() + AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume") + } + + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "outputVolume", let change { + if let oldValue = (change[.oldKey] as? NSNumber)?.doubleValue, let newValue = (change[.newKey] as? NSNumber)?.doubleValue { + if oldValue < newValue || newValue == 1.0 { + self.volumeUpDetectedPromise.set(.single(Void())) + } + } + } } private func updateCurrentAudioRouteInfo() { diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index d8424b9fb0..0c906f1779 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -912,6 +912,7 @@ public class Account { private var resetPeerHoleManagement: ((PeerId) -> Void)? public private(set) var pendingMessageManager: PendingMessageManager! + private(set) var pendingStoryManager: PendingStoryManager? public private(set) var pendingUpdateMessageManager: PendingUpdateMessageManager! private(set) var messageMediaPreuploadManager: MessageMediaPreuploadManager! private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext! @@ -1041,6 +1042,11 @@ public class Account { self.messageMediaPreuploadManager = MessageMediaPreuploadManager() self.pendingMessageManager = PendingMessageManager(network: network, postbox: postbox, accountPeerId: peerId, auxiliaryMethods: auxiliaryMethods, stateManager: self.stateManager, localInputActivityManager: self.localInputActivityManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext) + if !supplementary { + self.pendingStoryManager = PendingStoryManager(postbox: postbox, network: network, accountPeerId: peerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext, auxiliaryMethods: self.auxiliaryMethods) + } else { + self.pendingStoryManager = nil + } self.pendingUpdateMessageManager = PendingUpdateMessageManager(postbox: postbox, network: network, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext) self.pendingPeerMediaUploadManager = PendingPeerMediaUploadManager(postbox: postbox, network: network, stateManager: self.stateManager, accountPeerId: self.peerId) @@ -1155,6 +1161,7 @@ public class Account { let extractedExpr: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, self.pendingMessageManager.hasPendingMessages |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, + (self.pendingStoryManager?.hasPending ?? .single(false)) |> map { hasPending in hasPending ? AccountRunningImportantTasks.pendingMessages : [] }, self.pendingUpdateMessageManager.updatingMessageMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] }, diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 277cf02a3b..415773973d 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -38,6 +38,12 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id { copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox) } + let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame") + let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame") + if FileManager.default.fileExists(atPath: videoFirstFrameFromPath) { + let _ = try? FileManager.default.copyItem(atPath: videoFirstFrameFromPath, toPath: videoFirstFrameToPath) + } + if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType { copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift new file mode 100644 index 0000000000..b5c77d471c --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -0,0 +1,324 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramApi + +public extension Stories { + final class PendingItem: Equatable, Codable { + private enum CodingKeys: CodingKey { + case stableId + case timestamp + case media + case text + case entities + case pin + case privacy + case period + case randomId + } + + public let stableId: Int32 + public let timestamp: Int32 + public let media: Media + public let text: String + public let entities: [MessageTextEntity] + public let pin: Bool + public let privacy: EngineStoryPrivacy + public let period: Int32 + public let randomId: Int64 + + public init( + stableId: Int32, + timestamp: Int32, + media: Media, + text: String, + entities: [MessageTextEntity], + pin: Bool, + privacy: EngineStoryPrivacy, + period: Int32, + randomId: Int64 + ) { + self.stableId = stableId + self.timestamp = timestamp + self.media = media + self.text = text + self.entities = entities + self.pin = pin + self.privacy = privacy + self.period = period + self.randomId = randomId + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.stableId = try container.decode(Int32.self, forKey: .stableId) + self.timestamp = try container.decode(Int32.self, forKey: .timestamp) + + let mediaData = try container.decode(Data.self, forKey: .media) + self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as! Media + + self.text = try container.decode(String.self, forKey: .text) + self.entities = try container.decode([MessageTextEntity].self, forKey: .entities) + self.pin = try container.decode(Bool.self, forKey: .pin) + self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy) + self.period = try container.decode(Int32.self, forKey: .period) + self.randomId = try container.decode(Int64.self, forKey: .randomId) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.stableId, forKey: .stableId) + try container.encode(self.timestamp, forKey: .timestamp) + + let mediaEncoder = PostboxEncoder() + mediaEncoder.encodeRootObject(self.media) + try container.encode(mediaEncoder.makeData(), forKey: .media) + + try container.encode(self.text, forKey: .text) + try container.encode(self.entities, forKey: .entities) + try container.encode(self.pin, forKey: .pin) + try container.encode(self.privacy, forKey: .privacy) + try container.encode(self.period, forKey: .period) + try container.encode(self.randomId, forKey: .randomId) + } + + public static func ==(lhs: PendingItem, rhs: PendingItem) -> Bool { + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.stableId != rhs.stableId { + return false + } + if !lhs.media.isEqual(to: rhs.media) { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.entities != rhs.entities { + return false + } + if lhs.pin != rhs.pin { + return false + } + if lhs.privacy != rhs.privacy { + return false + } + if lhs.period != rhs.period { + return false + } + if lhs.randomId != rhs.randomId { + return false + } + return true + } + } + + struct LocalState: Equatable, Codable { + public var items: [PendingItem] + + public init( + items: [PendingItem] + ) { + self.items = items + } + } +} + +final class PendingStoryManager { + private final class PendingItemContext { + let queue: Queue + let item: Stories.PendingItem + let updated: () -> Void + + var progress: Float = 0.0 + var disposable: Disposable? + + init(queue: Queue, item: Stories.PendingItem, updated: @escaping () -> Void) { + self.queue = queue + self.item = item + self.updated = updated + } + + deinit { + self.disposable?.dispose() + } + } + + private final class Impl { + let queue: Queue + let postbox: Postbox + let network: Network + let accountPeerId: PeerId + let stateManager: AccountStateManager + let messageMediaPreuploadManager: MessageMediaPreuploadManager + let revalidationContext: MediaReferenceRevalidationContext + let auxiliaryMethods: AccountAuxiliaryMethods + + var itemsDisposable: Disposable? + var currentPendingItemContext: PendingItemContext? + + var storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:] + + private let allStoriesUploadProgressPromise = ValuePromise(nil, ignoreRepeated: true) + var allStoriesUploadProgress: Signal { + return self.allStoriesUploadProgressPromise.get() + } + + private let hasPendingPromise = ValuePromise(false, ignoreRepeated: true) + var hasPending: Signal { + return self.hasPendingPromise.get() + } + + func storyUploadProgress(stableId: Int32, next: @escaping (Float) -> Void) -> Disposable { + let bag: Bag<(Float) -> Void> + if let current = self.storyObserverContexts[stableId] { + bag = current + } else { + bag = Bag() + self.storyObserverContexts[stableId] = bag + } + + let index = bag.add(next) + if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId { + next(currentPendingItemContext.progress) + } else { + next(1.0) + } + + let queue = self.queue + return ActionDisposable { [weak self, weak bag] in + queue.async { + guard let `self` = self else { + return + } + if let bag, let listBag = self.storyObserverContexts[stableId], listBag === bag { + bag.remove(index) + if bag.isEmpty { + self.storyObserverContexts.removeValue(forKey: stableId) + } + } + } + } + } + + init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) { + self.queue = queue + self.postbox = postbox + self.network = network + self.accountPeerId = accountPeerId + self.stateManager = stateManager + self.messageMediaPreuploadManager = messageMediaPreuploadManager + self.revalidationContext = revalidationContext + self.auxiliaryMethods = auxiliaryMethods + + self.itemsDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .local)]) + |> deliverOn(self.queue)).start(next: { [weak self] views in + guard let `self` = self else { + return + } + guard let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView else { + return + } + let localState: Stories.LocalState + if let value = view.value?.get(Stories.LocalState.self) { + localState = value + } else { + localState = Stories.LocalState(items: []) + } + self.update(localState: localState) + }) + } + + deinit { + self.itemsDisposable?.dispose() + } + + private func update(localState: Stories.LocalState) { + if let currentPendingItemContext = self.currentPendingItemContext, !localState.items.contains(where: { $0.randomId == currentPendingItemContext.item.randomId }) { + self.currentPendingItemContext = nil + } + + if self.currentPendingItemContext == nil, let firstItem = localState.items.first { + let queue = self.queue + let itemStableId = firstItem.stableId + let pendingItemContext = PendingItemContext(queue: queue, item: firstItem, updated: { [weak self] in + queue.async { + guard let `self` = self else { + return + } + self.processContextsUpdated() + if let pendingItemContext = self.currentPendingItemContext, pendingItemContext.item.stableId == itemStableId, let bag = self.storyObserverContexts[itemStableId] { + for f in bag.copyItems() { + f(pendingItemContext.progress) + } + } + } + }) + self.currentPendingItemContext = pendingItemContext + + let stableId = firstItem.stableId + pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, period: Int(firstItem.period), randomId: firstItem.randomId) + |> deliverOn(self.queue)).start(next: { [weak self] event in + guard let `self` = self else { + return + } + switch event { + case let .progress(progress): + if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId { + currentPendingItemContext.progress = progress + currentPendingItemContext.updated() + } + case .completed: + // wait for the local state to change via Postbox + break + } + }) + } + + self.processContextsUpdated() + } + + private func processContextsUpdated() { + self.allStoriesUploadProgressPromise.set(self.currentPendingItemContext?.progress) + self.hasPendingPromise.set(self.currentPendingItemContext != nil) + } + } + + private let queue: Queue + private let impl: QueueLocalObject + private let accountPeerId: PeerId + + public var allStoriesUploadProgress: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.allStoriesUploadProgress.start(next: subscriber.putNext) + } + } + + public var hasPending: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.hasPending.start(next: subscriber.putNext) + } + } + + public func storyUploadProgress(stableId: Int32) -> Signal { + return self.impl.signalWith { impl, subscriber in + return impl.storyUploadProgress(stableId: stableId, next: subscriber.putNext) + } + } + + init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) { + let queue = Queue.mainQueue() + self.queue = queue + self.accountPeerId = accountPeerId + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods) + }) + } + + func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? { + return _internal_lookUpPendingStoryIdMapping(accountPeerId: self.accountPeerId, stableId: stableId) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index d4472bc68e..f19c83a8ac 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -5,10 +5,10 @@ import TelegramApi public enum EngineStoryInputMedia { case image(dimensions: PixelDimensions, data: Data) - case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource) + case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?) } -public struct EngineStoryPrivacy: Equatable { +public struct EngineStoryPrivacy: Codable, Equatable { public typealias Base = Stories.Item.Privacy.Base public var base: Base @@ -66,11 +66,27 @@ public enum Stories { case additionallyIncludePeers = "addPeers" } - public enum Base: Int32 { + public enum Base: Int32, Codable { + private enum CodingKeys: CodingKey { + case value + } + case everyone = 0 case contacts = 1 case closeFriends = 2 case nobody = 3 + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.init(rawValue: try container.decode(Int32.self, forKey: .value))! + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.rawValue, forKey: .value) + } } public var base: Base @@ -501,10 +517,7 @@ public enum StoryUploadResult { case completed(Int32?) } -private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal, media: Media) { - let originalMedia: Media - let contentToUpload: MessageContentToUpload - +private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media { switch media { case let .image(dimensions, data): let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) @@ -518,31 +531,21 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia partialReference: nil, flags: [] ) - originalMedia = imageMedia + return imageMedia + case let .video(dimensions, duration, resource, firstFrameImageData): + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if let firstFrameImageData = firstFrameImageData { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + account.postbox.mediaBox.storeResourceData(resource.id, data: firstFrameImageData) + + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + } - contentToUpload = messageContentToUpload( - accountPeerId: account.peerId, - network: account.network, - postbox: account.postbox, - auxiliaryMethods: account.auxiliaryMethods, - transformOutgoingMessageMedia: nil, - messageMediaPreuploadManager: account.messageMediaPreuploadManager, - revalidationContext: account.mediaReferenceRevalidationContext, - forceReupload: true, - isGrouped: false, - passFetchProgress: false, - peerId: account.peerId, - messageId: nil, - attributes: [], - text: "", - media: [imageMedia] - ) - case let .video(dimensions, duration, resource): let fileMedia = TelegramMediaFile( fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: MediaId.Id.random(in: MediaId.Id.min ... MediaId.Id.max)), partialReference: nil, resource: resource, - previewRepresentations: [], + previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", @@ -551,26 +554,32 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil) ] ) - originalMedia = fileMedia - contentToUpload = messageContentToUpload( - accountPeerId: account.peerId, - network: account.network, - postbox: account.postbox, - auxiliaryMethods: account.auxiliaryMethods, - transformOutgoingMessageMedia: nil, - messageMediaPreuploadManager: account.messageMediaPreuploadManager, - revalidationContext: account.mediaReferenceRevalidationContext, - forceReupload: true, - isGrouped: false, - passFetchProgress: true, - peerId: account.peerId, - messageId: nil, - attributes: [], - text: "", - media: [fileMedia] - ) + return fileMedia } +} + +private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) -> (signal: Signal, media: Media) { + let originalMedia: Media = media + let contentToUpload: MessageContentToUpload + + contentToUpload = messageContentToUpload( + accountPeerId: accountPeerId, + network: network, + postbox: postbox, + auxiliaryMethods: auxiliaryMethods, + transformOutgoingMessageMedia: nil, + messageMediaPreuploadManager: messageMediaPreuploadManager, + revalidationContext: revalidationContext, + forceReupload: true, + isGrouped: false, + passFetchProgress: false, + peerId: accountPeerId, + messageId: nil, + attributes: [], + text: "", + media: [media] + ) let contentSignal: Signal switch contentToUpload { @@ -624,15 +633,82 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran return privacyRules } -func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal { - let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media) +func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) { + let inputMedia = prepareUploadStoryContent(account: account, media: media) + + let _ = (account.postbox.transaction { transaction in + var currentState: Stories.LocalState + if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { + currentState = value + } else { + currentState = Stories.LocalState(items: []) + } + var stableId: Int32 = Int32.random(in: 2000000 ..< Int32.max) + while currentState.items.contains(where: { $0.stableId == stableId }) { + stableId = Int32.random(in: 2000000 ..< Int32.max) + } + currentState.items.append(Stories.PendingItem( + stableId: stableId, + timestamp: Int32(Date().timeIntervalSince1970), + media: inputMedia, + text: text, + entities: entities, + pin: pin, + privacy: privacy, + period: Int32(period), + randomId: randomId + )) + transaction.setLocalStoryState(state: CodableEntry(currentState)) + }).start() +} + +func _internal_cancelStoryUpload(account: Account, stableId: Int32) { + let _ = (account.postbox.transaction { transaction in + var currentState: Stories.LocalState + if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { + currentState = value + } else { + currentState = Stories.LocalState(items: []) + } + if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) { + currentState.items.remove(at: index) + transaction.setLocalStoryState(state: CodableEntry(currentState)) + } + }).start() +} + +private struct PendingStoryIdMappingKey: Hashable { + var accountPeerId: PeerId + var stableId: Int32 +} + +private let pendingStoryIdMapping = Atomic<[PendingStoryIdMappingKey: Int32]>(value: [:]) + +func _internal_lookUpPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32) -> Int32? { + return pendingStoryIdMapping.with { dict in + return dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)] + } +} + +private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32, id: Int32) { + let _ = pendingStoryIdMapping.modify { dict in + var dict = dict + + dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)] = id + + return dict + } +} + +func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal { + let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods) return contentSignal |> mapToSignal { result -> Signal in switch result { case let .progress(progress): return .single(.progress(progress)) case let .content(content): - return account.postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction) switch content.content { case let .media(inputMedia, _): @@ -664,7 +740,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: flags |= 1 << 3 - return account.network.request(Api.functions.stories.sendStory( + return network.request(Api.functions.stories.sendStory( flags: flags, media: inputMedia, caption: apiCaption, @@ -678,27 +754,69 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: return .single(nil) } |> mapToSignal { updates -> Signal in - var id: Int32? - if let updates = updates { - for update in updates.allUpdates { - if case let .updateStory(_, story) = update { - switch story { - case let .storyItem(_, idValue, _, _, _, _, media, _, _): - id = idValue - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) - if let parsedMedia = parsedMedia { - applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) - } - default: - break - } - } + return postbox.transaction { transaction -> StoryUploadResult in + var currentState: Stories.LocalState + if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { + currentState = value + } else { + currentState = Stories.LocalState(items: []) + } + if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) { + currentState.items.remove(at: index) + transaction.setLocalStoryState(state: CodableEntry(currentState)) } - account.stateManager.addUpdates(updates) + var id: Int32? + if let updates = updates { + for update in updates.allUpdates { + if case let .updateStory(_, story) = update { + switch story { + case let .storyItem(_, idValue, _, _, _, _, media, _, _): + if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) { + var items = transaction.getStoryItems(peerId: accountPeerId) + var updatedItems: [Stories.Item] = [] + if items.firstIndex(where: { $0.id == id }) == nil, case let .item(item) = parsedStory { + let updatedItem = Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: item.views, + privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { + items.append(StoryItemsTableEntry(value: entry, id: item.id)) + } + updatedItems.append(updatedItem) + } + transaction.setStoryItems(peerId: accountPeerId, items: items) + } + + id = idValue + let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, accountPeerId) + if let parsedMedia = parsedMedia { + applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile) + } + default: + break + } + } + } + + if let id = id { + _internal_putPendingStoryIdMapping(accountPeerId: accountPeerId, stableId: stableId, id: id) + } + + stateManager.addUpdates(updates) + } + + return .completed(id) } - - return .single(.completed(id)) } default: return .complete() @@ -715,7 +833,7 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In let contentSignal: Signal let originalMedia: Media? if let media = media { - (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media) + (contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods) } else { contentSignal = .single(nil) originalMedia = nil @@ -803,6 +921,73 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In } } +func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStoryPrivacy) -> Signal { + return account.postbox.transaction { transaction -> [Api.InputPrivacyRule] in + let storyId = StoryId(peerId: account.peerId, id: id) + if let storyItem = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self), case let .item(item) = storyItem { + let updatedItem = Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: item.views, + privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { + transaction.setStory(id: storyId, value: entry) + } + } + + var items = transaction.getStoryItems(peerId: account.peerId) + var updatedItems: [Stories.Item] = [] + if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) { + let updatedItem = Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + text: item.text, + entities: item.entities, + views: item.views, + privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic + ) + if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { + items[index] = StoryItemsTableEntry(value: entry, id: item.id) + } + + updatedItems.append(updatedItem) + } + transaction.setStoryItems(peerId: account.peerId, items: items) + + return apiInputPrivacyRules(privacy: privacy, transaction: transaction) + } + |> mapToSignal { inputRules -> Signal in + var flags: Int32 = 0 + flags |= 1 << 2 + + return account.network.request(Api.functions.stories.editStory(flags: flags, id: id, media: nil, caption: nil, entities: nil, privacyRules: inputRules)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + + return .complete() + } + } +} + func _internal_deleteStories(account: Account, ids: [Int32]) -> Signal { return account.postbox.transaction { transaction -> Void in var items = transaction.getStoryItems(peerId: account.peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index ec1b07e474..f95de0c04e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -42,8 +42,9 @@ public final class EngineStoryItem: Equatable { public let isPinned: Bool public let isExpired: Bool public let isPublic: Bool + public let isPending: Bool - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -55,6 +56,7 @@ public final class EngineStoryItem: Equatable { self.isPinned = isPinned self.isExpired = isExpired self.isPublic = isPublic + self.isPending = isPending } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -91,6 +93,9 @@ public final class EngineStoryItem: Equatable { if lhs.isPublic != rhs.isPublic { return false } + if lhs.isPending != rhs.isPending { + return false + } return true } } @@ -474,7 +479,8 @@ public final class PeerStoryListContext { privacy: item.privacy.flatMap(EngineStoryPrivacy.init), isPinned: item.isPinned, isExpired: item.isExpired, - isPublic: item.isPublic + isPublic: item.isPublic, + isPending: false ) items.append(mappedItem) } @@ -578,7 +584,8 @@ public final class PeerStoryListContext { privacy: item.privacy.flatMap(EngineStoryPrivacy.init), isPinned: item.isPinned, isExpired: item.isExpired, - isPublic: item.isPublic + isPublic: item.isPublic, + isPending: false ) storyItems.append(mappedItem) } @@ -709,7 +716,8 @@ public final class PeerStoryListContext { privacy: item.privacy.flatMap(EngineStoryPrivacy.init), isPinned: item.isPinned, isExpired: item.isExpired, - isPublic: item.isPublic + isPublic: item.isPublic, + isPending: false ) finalUpdatedState = updatedState } @@ -745,7 +753,8 @@ public final class PeerStoryListContext { privacy: item.privacy.flatMap(EngineStoryPrivacy.init), isPinned: item.isPinned, isExpired: item.isExpired, - isPublic: item.isPublic + isPublic: item.isPublic, + isPending: false )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -890,7 +899,8 @@ public final class PeerExpiringStoryListContext { privacy: item.privacy.flatMap(EngineStoryPrivacy.init), isPinned: item.isPinned, isExpired: item.isExpired, - isPublic: item.isPublic + isPublic: item.isPublic, + isPending: false ) items.append(.item(mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index a3b9667b44..141f9c9e52 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -902,14 +902,40 @@ public extension TelegramEngine { } } - public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal { - return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId) + public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) { + _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId) + } + + public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? { + return self.account.pendingStoryManager?.lookUpPendingStoryIdMapping(stableId: stableId) + } + + public func allStoriesUploadProgress() -> Signal { + guard let pendingStoryManager = self.account.pendingStoryManager else { + return .single(nil) + } + return pendingStoryManager.allStoriesUploadProgress + } + + public func storyUploadProgress(stableId: Int32) -> Signal { + guard let pendingStoryManager = self.account.pendingStoryManager else { + return .single(0.0) + } + return pendingStoryManager.storyUploadProgress(stableId: stableId) + } + + public func cancelStoryUpload(stableId: Int32) { + _internal_cancelStoryUpload(account: self.account, stableId: stableId) } public func editStory(media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal { return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy) } + public func editStoryPrivacy(id: Int32, privacy: EngineStoryPrivacy) -> Signal { + return _internal_editStoryPrivacy(account: self.account, id: id, privacy: privacy) + } + public func deleteStories(ids: [Int32]) -> Signal { return _internal_deleteStories(account: self.account, ids: ids) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 1adb3e90b4..c944fc7b3b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -375,6 +375,7 @@ swift_library( "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", + "//submodules/Utils/VolumeButtons", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 0527392b00..43479b6fa9 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -828,6 +828,7 @@ public final class ChatListHeaderComponent: Component { context: component.context, theme: component.theme, strings: component.strings, + sideInset: component.sideInset, includesHidden: component.storiesIncludeHidden, storySubscriptions: storySubscriptions, collapseFraction: 1.0 - component.storiesFraction, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 1a3479811c..dd9d24e35f 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -163,6 +163,8 @@ public final class ChatListNavigationBar: Component { private weak var disappearingTabsView: UIView? private var disappearingTabsViewSearch: Bool = false + private var currentHeaderComponent: ChatListHeaderComponent? + override public init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0) @@ -332,35 +334,37 @@ public final class ChatListNavigationBar: Component { self.storiesOffsetFraction = storiesOffsetFraction self.storiesUnlockedFraction = storiesUnlockedOffsetFraction + let headerComponent = ChatListHeaderComponent( + sideInset: component.sideInset + 16.0, + primaryContent: component.primaryContent, + secondaryContent: component.secondaryContent, + secondaryTransition: component.secondaryTransition, + networkStatus: nil, + storySubscriptions: component.storySubscriptions, + storiesIncludeHidden: component.storiesIncludeHidden, + storiesFraction: 1.0 - storiesOffsetFraction, + storiesUnlockedFraction: 1.0 - storiesUnlockedOffsetFraction, + uploadProgress: component.uploadProgress, + context: component.context, + theme: component.theme, + strings: component.strings, + openStatusSetup: { [weak self] sourceView in + guard let self, let component = self.component else { + return + } + component.openStatusSetup(sourceView) + }, + toggleIsLocked: { [weak self] in + guard let self, let component = self.component else { + return + } + component.context.sharedContext.appLockContext.lock() + } + ) + self.currentHeaderComponent = headerComponent let headerContentSize = self.headerContent.update( transition: headerTransition, - component: AnyComponent(ChatListHeaderComponent( - sideInset: component.sideInset + 16.0, - primaryContent: component.primaryContent, - secondaryContent: component.secondaryContent, - secondaryTransition: component.secondaryTransition, - networkStatus: nil, - storySubscriptions: component.storySubscriptions, - storiesIncludeHidden: component.storiesIncludeHidden, - storiesFraction: 1.0 - storiesOffsetFraction, - storiesUnlockedFraction: 1.0 - storiesUnlockedOffsetFraction, - uploadProgress: component.uploadProgress, - context: component.context, - theme: component.theme, - strings: component.strings, - openStatusSetup: { [weak self] sourceView in - guard let self, let component = self.component else { - return - } - component.openStatusSetup(sourceView) - }, - toggleIsLocked: { [weak self] in - guard let self, let component = self.component else { - return - } - component.context.sharedContext.appLockContext.lock() - } - )), + component: AnyComponent(headerComponent), environment: {}, containerSize: CGSize(width: currentLayout.size.width, height: 44.0) ) @@ -439,6 +443,60 @@ public final class ChatListNavigationBar: Component { } } + public func updateStoryUploadProgress(storyUploadProgress: Float?) { + guard let component = self.component else { + return + } + if component.uploadProgress != storyUploadProgress { + self.component = ChatListNavigationBar( + context: component.context, + theme: component.theme, + strings: component.strings, + statusBarHeight: component.statusBarHeight, + sideInset: component.sideInset, + isSearchActive: component.isSearchActive, + storiesUnlocked: component.storiesUnlocked, + primaryContent: component.primaryContent, + secondaryContent: component.secondaryContent, + secondaryTransition: component.secondaryTransition, + storySubscriptions: component.storySubscriptions, + storiesIncludeHidden: component.storiesIncludeHidden, + uploadProgress: storyUploadProgress, + tabsNode: component.tabsNode, + tabsNodeIsSearch: component.tabsNodeIsSearch, + activateSearch: component.activateSearch, + openStatusSetup: component.openStatusSetup + ) + if let currentLayout = self.currentLayout, let headerComponent = self.currentHeaderComponent { + let headerComponent = ChatListHeaderComponent( + sideInset: headerComponent.sideInset, + primaryContent: headerComponent.primaryContent, + secondaryContent: headerComponent.secondaryContent, + secondaryTransition: headerComponent.secondaryTransition, + networkStatus: headerComponent.networkStatus, + storySubscriptions: headerComponent.storySubscriptions, + storiesIncludeHidden: headerComponent.storiesIncludeHidden, + storiesFraction: headerComponent.storiesFraction, + storiesUnlockedFraction: headerComponent.storiesUnlockedFraction, + uploadProgress: storyUploadProgress, + context: headerComponent.context, + theme: headerComponent.theme, + strings: headerComponent.strings, + openStatusSetup: headerComponent.openStatusSetup, + toggleIsLocked: headerComponent.toggleIsLocked + ) + self.currentHeaderComponent = headerComponent + + let _ = self.headerContent.update( + transition: .immediate, + component: AnyComponent(headerComponent), + environment: {}, + containerSize: CGSize(width: currentLayout.size.width, height: 44.0) + ) + } + } + } + func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index ab63183849..11faf768e3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -983,7 +983,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr transitionIn = StoryContainerScreen.TransitionIn( sourceView: self.view, sourceRect: self.itemGrid.view.convert(itemRect, to: self.view), - sourceCornerRadius: 0.0 + sourceCornerRadius: 0.0, + sourceIsAvatar: false ) } diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index 13cfe06639..130b28a8e8 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -7,6 +7,7 @@ public final class PlainButtonComponent: Component { public enum EffectAlignment { case left case right + case center } public let content: AnyComponent @@ -136,9 +137,18 @@ public final class PlainButtonComponent: Component { contentTransition.setAlpha(view: contentView, alpha: contentAlpha) } - self.contentContainer.layer.anchorPoint = CGPoint(x: component.effectAlignment == .left ? 0.0 : 1.0, y: 0.5) + let anchorX: CGFloat + switch component.effectAlignment { + case .left: + anchorX = 0.0 + case .center: + anchorX = 0.5 + case .right: + anchorX = 1.0 + } + self.contentContainer.layer.anchorPoint = CGPoint(x: anchorX, y: 0.5) transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size)) - transition.setPosition(view: self.contentContainer, position: CGPoint(x: component.effectAlignment == .left ? 0.0 : size.width, y: size.height * 0.5)) + transition.setPosition(view: self.contentContainer, position: CGPoint(x: size.width * anchorX, y: size.height * 0.5)) return size } diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index 0dd1a6dd55..f857f63b65 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -7,13 +7,19 @@ import TelegramPresentationData public final class AvatarStoryIndicatorComponent: Component { public let hasUnseen: Bool public let isDarkTheme: Bool + public let activeLineWidth: CGFloat + public let inactiveLineWidth: CGFloat public init( hasUnseen: Bool, - isDarkTheme: Bool + isDarkTheme: Bool, + activeLineWidth: CGFloat, + inactiveLineWidth: CGFloat ) { self.hasUnseen = hasUnseen self.isDarkTheme = isDarkTheme + self.activeLineWidth = activeLineWidth + self.inactiveLineWidth = inactiveLineWidth } public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { @@ -23,6 +29,12 @@ public final class AvatarStoryIndicatorComponent: Component { if lhs.isDarkTheme != rhs.isDarkTheme { return false } + if lhs.activeLineWidth != rhs.activeLineWidth { + return false + } + if lhs.inactiveLineWidth != rhs.inactiveLineWidth { + return false + } return true } @@ -50,23 +62,21 @@ public final class AvatarStoryIndicatorComponent: Component { let lineWidth: CGFloat let diameter: CGFloat - let outerInset: CGFloat if component.hasUnseen { - lineWidth = 3.0 - outerInset = 3.0 + lineWidth - diameter = availableSize.width + outerInset * 2.0 + lineWidth = component.activeLineWidth } else { - lineWidth = 2.0 - outerInset = 3.0 + lineWidth - diameter = availableSize.width + outerInset * 2.0 + lineWidth = component.inactiveLineWidth } + let maxOuterInset = component.activeLineWidth + component.activeLineWidth + diameter = availableSize.width + maxOuterInset * 2.0 + let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0 - self.indicatorView.image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in + self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setLineWidth(lineWidth) - context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) context.replacePathWithStrokedPath() context.clip() @@ -87,7 +97,7 @@ public final class AvatarStoryIndicatorComponent: Component { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) - transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(), size: availableSize).insetBy(dx: -outerInset, dy: -outerInset)) + transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))) return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 8d1b7d63ae..928b85104d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent", "//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/MediaEditorScreen", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramPresentationData", "//submodules/ReactionSelectionNode", "//submodules/ContextUI", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 3fc39633cf..2c230ec285 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -162,6 +162,7 @@ private final class StoryContainerScreenComponent: Component { private let focusedItem = ValuePromise(nil, ignoreRepeated: true) private var contentUpdatedDisposable: Disposable? + private let storyItemSharedState = StoryContentItem.SharedState() private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:] private var itemSetPanState: ItemSetPanState? @@ -171,6 +172,8 @@ private final class StoryContainerScreenComponent: Component { private var isAnimatingOut: Bool = false private var didAnimateOut: Bool = false + var dismissWithoutTransitionOut: Bool = false + override init(frame: CGRect) { self.backgroundLayer = SimpleLayer() self.backgroundLayer.backgroundColor = UIColor.black.cgColor @@ -467,7 +470,7 @@ private final class StoryContainerScreenComponent: Component { func animateOut(completion: @escaping () -> Void) { self.isAnimatingOut = true - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) { + if !self.dismissWithoutTransitionOut, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) { self.state?.updated(transition: .immediate) let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) @@ -482,12 +485,18 @@ private final class StoryContainerScreenComponent: Component { focusedItemPromise.set(.single(nil)) }) } else { + let transition: Transition + if self.dismissWithoutTransitionOut { + transition = Transition(animation: .curve(duration: 0.5, curve: .spring)) + } else { + transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.verticalPanState = ItemSetPanState(fraction: 1.0, didBegin: true) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + self.state?.updated(transition: transition) let focusedItemPromise = self.component?.focusedItemPromise - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in completion() focusedItemPromise?.set(.single(nil)) @@ -690,6 +699,7 @@ private final class StoryContainerScreenComponent: Component { component: AnyComponent(StoryItemSetContainerComponent( context: component.context, externalState: itemSetView.externalState, + storyItemSharedState: self.storyItemSharedState, slice: slice, theme: environment.theme, strings: environment.strings, @@ -989,15 +999,18 @@ public class StoryContainerScreen: ViewControllerComponentContainer { public weak var sourceView: UIView? public let sourceRect: CGRect public let sourceCornerRadius: CGFloat + public let sourceIsAvatar: Bool public init( sourceView: UIView, sourceRect: CGRect, - sourceCornerRadius: CGFloat + sourceCornerRadius: CGFloat, + sourceIsAvatar: Bool ) { self.sourceView = sourceView self.sourceRect = sourceRect self.sourceCornerRadius = sourceCornerRadius + self.sourceIsAvatar = sourceIsAvatar } } @@ -1081,6 +1094,15 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } } + func dismissWithoutTransitionOut() { + self.focusedItemPromise.set(.single(nil)) + + if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.dismissWithoutTransitionOut = true + } + self.dismiss() + } + override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index ccfce0b14b..a337cc0730 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -12,6 +12,13 @@ public final class StoryContentItem { } } + public final class SharedState { + public var useAmbientMode: Bool = true + + public init() { + } + } + open class View: UIView { open func setIsProgressPaused(_ isProgressPaused: Bool) { } @@ -22,15 +29,18 @@ public final class StoryContentItem { public final class Environment: Equatable { public let externalState: ExternalState + public let sharedState: SharedState public let presentationProgressUpdated: (Double, Bool) -> Void public let markAsSeen: (StoryId) -> Void public init( externalState: ExternalState, + sharedState: SharedState, presentationProgressUpdated: @escaping (Double, Bool) -> Void, markAsSeen: @escaping (StoryId) -> Void ) { self.externalState = externalState + self.sharedState = sharedState self.presentationProgressUpdated = presentationProgressUpdated self.markAsSeen = markAsSeen } @@ -39,6 +49,9 @@ public final class StoryContentItem { if lhs.externalState !== rhs.externalState { return false } + if lhs.sharedState !== rhs.sharedState { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 5e1618c607..b72b2123e1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -41,6 +41,22 @@ final class StoryContentCaptionComponent: Component { } return true } + + private struct ItemLayout { + var containerSize: CGSize + var visibleTextHeight: CGFloat + var verticalInset: CGFloat + + init( + containerSize: CGSize, + visibleTextHeight: CGFloat, + verticalInset: CGFloat + ) { + self.containerSize = containerSize + self.visibleTextHeight = visibleTextHeight + self.verticalInset = verticalInset + } + } final class View: UIView, UIScrollViewDelegate { private let scrollViewContainer: UIView @@ -51,15 +67,23 @@ final class StoryContentCaptionComponent: Component { private let scrollCenterMaskView: UIView private let scrollBottomMaskView: UIImageView + private let shadowGradientLayer: SimpleGradientLayer + private let shadowPlainLayer: SimpleLayer + private let text = ComponentView() private var component: StoryContentCaptionComponent? private weak var state: EmptyComponentState? + private var itemLayout: ItemLayout? + private var ignoreScrolling: Bool = false private var ignoreExternalState: Bool = false override init(frame: CGRect) { + self.shadowGradientLayer = SimpleGradientLayer() + self.shadowPlainLayer = SimpleLayer() + self.scrollViewContainer = UIView() self.scrollView = UIScrollView() @@ -87,6 +111,9 @@ final class StoryContentCaptionComponent: Component { self.scrollMaskContainer.addSubview(self.scrollBottomMaskView) super.init(frame: frame) + + self.layer.addSublayer(self.shadowGradientLayer) + self.layer.addSublayer(self.shadowPlainLayer) self.scrollViewContainer.addSubview(self.scrollView) self.scrollView.delegate = self @@ -145,7 +172,7 @@ final class StoryContentCaptionComponent: Component { } private func updateScrolling(transition: Transition) { - guard let component = self.component else { + guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -155,6 +182,11 @@ final class StoryContentCaptionComponent: Component { let edgeDistanceFraction = edgeDistance / 7.0 transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction) + let shadowOverflow: CGFloat = 26.0 + let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) + transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame) + transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0))) + let expandDistance: CGFloat = 50.0 var expandFraction: CGFloat = self.scrollView.contentOffset.y / expandDistance expandFraction = max(0.0, min(1.0, expandFraction)) @@ -205,6 +237,12 @@ final class StoryContentCaptionComponent: Component { textView.frame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textSize) } + self.itemLayout = ItemLayout( + containerSize: availableSize, + visibleTextHeight: visibleTextHeight, + verticalInset: verticalInset + ) + self.ignoreScrolling = true if self.scrollView.contentSize != scrollContentSize { @@ -213,6 +251,28 @@ final class StoryContentCaptionComponent: Component { transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + if self.shadowGradientLayer.colors == nil { + var locations: [NSNumber] = [] + var colors: [CGColor] = [] + let numStops = 10 + let baseAlpha: CGFloat = 0.3 + for i in 0 ..< numStops { + let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1) + locations.append((1.0 - step) as NSNumber) + let alphaStep: CGFloat = pow(step, 1.2) + colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor) + } + + self.shadowGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0) + self.shadowGradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0) + + self.shadowGradientLayer.locations = locations + self.shadowGradientLayer.colors = colors + self.shadowGradientLayer.type = .axial + + self.shadowPlainLayer.backgroundColor = UIColor(white: 0.0, alpha: baseAlpha).cgColor + } + self.ignoreScrolling = false self.updateScrolling(transition: transition) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a69b8f8842..dbbf4a205e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -20,6 +20,7 @@ import AvatarNode import MediaEditorScreen import ImageCompression import ShareWithPeersScreen +import PlainButtonComponent public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -37,6 +38,7 @@ public final class StoryItemSetContainerComponent: Component { public let context: AccountContext public let externalState: ExternalState + public let storyItemSharedState: StoryContentItem.SharedState public let slice: StoryContentContextState.FocusedSlice public let theme: PresentationTheme public let strings: PresentationStrings @@ -59,6 +61,7 @@ public final class StoryItemSetContainerComponent: Component { public init( context: AccountContext, externalState: ExternalState, + storyItemSharedState: StoryContentItem.SharedState, slice: StoryContentContextState.FocusedSlice, theme: PresentationTheme, strings: PresentationStrings, @@ -80,6 +83,7 @@ public final class StoryItemSetContainerComponent: Component { ) { self.context = context self.externalState = externalState + self.storyItemSharedState = storyItemSharedState self.slice = slice self.theme = theme self.strings = strings @@ -531,6 +535,7 @@ public final class StoryItemSetContainerComponent: Component { let itemEnvironment = StoryContentItem.Environment( externalState: visibleItem.externalState, + sharedState: component.storyItemSharedState, presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in guard let self = self, let component = self.component else { return @@ -667,11 +672,13 @@ public final class StoryItemSetContainerComponent: Component { } if let rightInfoView = self.rightInfoItem?.view.view { - if transitionIn.sourceCornerRadius != 0.0 { + if transitionIn.sourceIsAvatar { let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), elevation: 0.0, duration: 0.3, curve: .spring, reverse: false) rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", additive: true) rightInfoView.layer.animateScale(from: innerSourceLocalFrame.width / rightInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } else { + rightInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } @@ -1553,6 +1560,12 @@ public final class StoryItemSetContainerComponent: Component { self.contextController = contextController self.updateIsProgressPaused() controller.present(contextController, in: .window(.root)) + }, + openPeer: { [weak self] peer in + guard let self else { + return + } + self.navigateToPeer(peer: peer) } )), environment: {}, @@ -1658,7 +1671,12 @@ public final class StoryItemSetContainerComponent: Component { let rightInfoItemSize = currentRightInfoItem.view.update( transition: .immediate, - component: currentRightInfoItem.component, + component: AnyComponent(PlainButtonComponent(content: currentRightInfoItem.component, effectAlignment: .center, action: { [weak self] in + guard let self, let component = self.component else { + return + } + self.navigateToPeer(peer: component.slice.peer) + })), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) ) @@ -1762,13 +1780,15 @@ public final class StoryItemSetContainerComponent: Component { containerSize: CGSize(width: availableSize.width, height: contentFrame.height) ) captionItem.view.parentState = state - let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.maxY - captionSize.height), size: captionSize) + let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.height - captionSize.height), size: captionSize) if let captionItemView = captionItem.view.view { if captionItemView.superview == nil { - self.addSubview(captionItemView) + if self.contentContainerView.subviews.count >= 1 { + self.contentContainerView.insertSubview(captionItemView, at: 1) + } } captionItemTransition.setFrame(view: captionItemView, frame: captionFrame) - captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList || self.inputPanelExternalState.isEditing) ? 0.0 : 1.0) } } @@ -2108,10 +2128,18 @@ public final class StoryItemSetContainerComponent: Component { initialPrivacy: privacy, stateContext: stateContext, completion: { [weak self] privacy in - self?.updateIsProgressPaused() - }, - editCategory: { privacy in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start() + self.updateIsProgressPaused() + }, + editCategory: { [weak self] privacy in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start() } ) self.component?.controller()?.push(controller) @@ -2121,6 +2149,33 @@ public final class StoryItemSetContainerComponent: Component { }) } + private func navigateToPeer(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in + guard let controller, let navigationController else { + return + } + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: animated) + })) + + controller.dismissWithoutTransitionOut() + } + private func openStoryEditing() { guard let context = self.component?.context, let id = self.component?.slice.item.storyItem.id else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index c4a480f7d9..0db897e77a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -32,6 +32,7 @@ final class StoryItemSetViewListComponent: Component { let expandViewStats: () -> Void let deleteAction: () -> Void let moreAction: (UIView, ContextGesture?) -> Void + let openPeer: (EnginePeer) -> Void init( externalState: ExternalState, @@ -44,7 +45,8 @@ final class StoryItemSetViewListComponent: Component { close: @escaping () -> Void, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, - moreAction: @escaping (UIView, ContextGesture?) -> Void + moreAction: @escaping (UIView, ContextGesture?) -> Void, + openPeer: @escaping (EnginePeer) -> Void ) { self.externalState = externalState self.context = context @@ -57,6 +59,7 @@ final class StoryItemSetViewListComponent: Component { self.expandViewStats = expandViewStats self.deleteAction = deleteAction self.moreAction = moreAction + self.openPeer = openPeer } static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool { @@ -432,8 +435,11 @@ final class StoryItemSetViewListComponent: Component { subtitle: dateText, selectionState: .none, hasNext: index != viewListState.totalCount - 1, - action: { _ in - + action: { [weak self] peer in + guard let self, let component = self.component else { + return + } + component.openPeer(peer) } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD index de30c2ebad..e8d4b8eba6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUniversalVideoContent", "//submodules/AvatarNode", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Utils/VolumeButtons", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryAuthorInfoComponent.swift index d58df3f96f..2c44123d0d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryAuthorInfoComponent.swift @@ -58,8 +58,16 @@ final class StoryAuthorInfoComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - let title = component.peer?.debugDisplayTitle ?? "" - let subtitle = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: component.timestamp).string + let title: String + if component.peer?.id == component.context.account.peerId { + //TODO:localize + title = "Your story" + } else { + title = component.peer?.debugDisplayTitle ?? "" + } + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp) let titleSize = self.title.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index d3cbbc93b1..fdf35dc62f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -28,13 +28,14 @@ public final class StoryContentContextImpl: StoryContentContext { private var disposable: Disposable? private var loadDisposable: Disposable? - private let currentFocusedIdPromise = Promise() + private let currentFocusedIdUpdatedPromise = Promise() private var storedFocusedId: Int32? + private var currentMappedItems: [EngineStoryItem]? var currentFocusedId: Int32? { didSet { if self.currentFocusedId != self.storedFocusedId { self.storedFocusedId = self.currentFocusedId - self.currentFocusedIdPromise.set(.single(self.currentFocusedId)) + self.currentFocusedIdUpdatedPromise.set(.single(Void())) } } } @@ -43,20 +44,25 @@ public final class StoryContentContextImpl: StoryContentContext { self.context = context self.peerId = peerId - self.currentFocusedIdPromise.set(.single(initialFocusedId)) + self.currentFocusedId = initialFocusedId + self.currentFocusedIdUpdatedPromise.set(.single(Void())) + var inputKeys: [PostboxViewKey] = [ + PostboxViewKey.basicPeer(peerId), + PostboxViewKey.storiesState(key: .peer(peerId)), + PostboxViewKey.storyItems(peerId: peerId) + ] + if peerId == context.account.peerId { + inputKeys.append(PostboxViewKey.storiesState(key: .local)) + } self.disposable = (combineLatest(queue: .mainQueue(), - self.currentFocusedIdPromise.get(), + self.currentFocusedIdUpdatedPromise.get(), context.account.postbox.combinedView( - keys: [ - PostboxViewKey.basicPeer(peerId), - PostboxViewKey.storiesState(key: .peer(peerId)), - PostboxViewKey.storyItems(peerId: peerId) - ] + keys: inputKeys ) ) - |> mapToSignal { currentFocusedId, views -> Signal<(Int32?, CombinedView, [PeerId: Peer]), NoError> in - return context.account.postbox.transaction { transaction -> (Int32?, CombinedView, [PeerId: Peer]) in + |> mapToSignal { _, views -> Signal<(CombinedView, [PeerId: Peer]), NoError> in + return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer]) in var peers: [PeerId: Peer] = [:] if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView { for item in itemsView.items { @@ -71,10 +77,10 @@ public final class StoryContentContextImpl: StoryContentContext { } } } - return (currentFocusedId, views, peers) + return (views, peers) } } - |> deliverOnMainQueue).start(next: { [weak self] currentFocusedId, views, peers in + |> deliverOnMainQueue).start(next: { [weak self] views, peers in guard let self else { return } @@ -84,7 +90,7 @@ public final class StoryContentContextImpl: StoryContentContext { guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else { return } - guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { + guard let peerStoryItemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { return } guard let peer = peerView.peer.flatMap(EnginePeer.init) else { @@ -92,86 +98,134 @@ public final class StoryContentContextImpl: StoryContentContext { } let state = stateView.value?.get(Stories.PeerState.self) + var mappedItems: [EngineStoryItem] = peerStoryItemsView.items.compactMap { item -> EngineStoryItem? in + guard case let .item(item) = item.value.get(Stories.StoredItem.self) else { + return nil + } + guard let media = item.media else { + return nil + } + return EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false + ) + } + if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { + for item in localState.items { + mappedItems.append(EngineStoryItem( + id: item.stableId, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + text: item.text, + entities: item.entities, + views: nil, + privacy: item.privacy, + isPinned: item.pin, + isExpired: false, + isPublic: false, + isPending: true + )) + } + } + + let currentFocusedId = self.storedFocusedId + var focusedIndex: Int? if let currentFocusedId { - focusedIndex = itemsView.items.firstIndex(where: { $0.id == currentFocusedId }) + focusedIndex = mappedItems.firstIndex(where: { $0.id == currentFocusedId }) + if focusedIndex == nil { + if let currentMappedItems = self.currentMappedItems { + if let previousIndex = currentMappedItems.firstIndex(where: { $0.id == currentFocusedId }) { + if currentMappedItems[previousIndex].isPending { + if let updatedId = context.engine.messages.lookUpPendingStoryIdMapping(stableId: currentFocusedId) { + if let index = mappedItems.firstIndex(where: { $0.id == updatedId }) { + focusedIndex = index + } + } + } + + if focusedIndex == nil && previousIndex != 0 { + for index in (0 ..< previousIndex).reversed() { + if let value = mappedItems.firstIndex(where: { $0.id == currentMappedItems[index].id }) { + focusedIndex = value + break + } + } + } + } + } + } } if focusedIndex == nil, let state { if let storedFocusedId = self.storedFocusedId { - focusedIndex = itemsView.items.firstIndex(where: { $0.id >= storedFocusedId }) + focusedIndex = mappedItems.firstIndex(where: { $0.id >= storedFocusedId }) + } else if let index = mappedItems.firstIndex(where: { $0.isPending }) { + focusedIndex = index } else { - focusedIndex = itemsView.items.firstIndex(where: { $0.id > state.maxReadId }) + focusedIndex = mappedItems.firstIndex(where: { $0.id > state.maxReadId }) } } if focusedIndex == nil { - if !itemsView.items.isEmpty { + if !mappedItems.isEmpty { focusedIndex = 0 } } + self.currentMappedItems = mappedItems + if let focusedIndex { - self.storedFocusedId = itemsView.items[focusedIndex].id + self.storedFocusedId = mappedItems[focusedIndex].id var previousItemId: Int32? var nextItemId: Int32? if focusedIndex != 0 { - previousItemId = itemsView.items[focusedIndex - 1].id + previousItemId = mappedItems[focusedIndex - 1].id } - if focusedIndex != itemsView.items.count - 1 { - nextItemId = itemsView.items[focusedIndex + 1].id + if focusedIndex != mappedItems.count - 1 { + nextItemId = mappedItems[focusedIndex + 1].id } var loadKeys: [StoryKey] = [] - for index in (focusedIndex - 2) ... (focusedIndex + 2) { - if index >= 0 && index < itemsView.items.count { - if let item = itemsView.items[index].value.get(Stories.StoredItem.self), case .placeholder = item { - loadKeys.append(StoryKey(peerId: peerId, id: item.id)) + if let mappedFocusedIndex = peerStoryItemsView.items.firstIndex(where: { $0.id == mappedItems[focusedIndex].id }) { + for index in (mappedFocusedIndex - 2) ... (mappedFocusedIndex + 2) { + if index >= 0 && index < peerStoryItemsView.items.count { + if let item = peerStoryItemsView.items[index].value.get(Stories.StoredItem.self), case .placeholder = item { + loadKeys.append(StoryKey(peerId: peerId, id: item.id)) + } } } - } - if !loadKeys.isEmpty { - loadIds(loadKeys) + if !loadKeys.isEmpty { + loadIds(loadKeys) + } } - if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media { - let mappedItem = EngineStoryItem( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: EngineMedia(media), - text: item.text, - entities: item.entities, - views: item.views.flatMap { views in - return EngineStoryItem.Views( - seenCount: views.seenCount, - seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in - return peers[id].flatMap(EnginePeer.init) - } - ) - }, - privacy: item.privacy.flatMap(EngineStoryPrivacy.init), - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic - ) + do { + let mappedItem = mappedItems[focusedIndex] var nextItems: [EngineStoryItem] = [] - for i in (focusedIndex + 1) ..< min(focusedIndex + 4, itemsView.items.count) { - if let item = itemsView.items[i].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media { - nextItems.append(EngineStoryItem( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: EngineMedia(media), - text: item.text, - entities: item.entities, - views: nil, - privacy: item.privacy.flatMap(EngineStoryPrivacy.init), - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic - )) + for i in (focusedIndex + 1) ..< min(focusedIndex + 4, mappedItems.count) { + do { + let item = mappedItems[i] + nextItems.append(item) } } @@ -179,7 +233,7 @@ public final class StoryContentContextImpl: StoryContentContext { self.sliceValue = StoryContentContextState.FocusedSlice( peer: peer, item: StoryContentItem( - id: AnyHashable(item.id), + id: AnyHashable(mappedItem.id), position: focusedIndex, component: AnyComponent(StoryItemContentComponent( context: context, @@ -189,7 +243,7 @@ public final class StoryContentContextImpl: StoryContentContext { centerInfoComponent: AnyComponent(StoryAuthorInfoComponent( context: context, peer: peer, - timestamp: item.timestamp + timestamp: mappedItem.timestamp )), rightInfoComponent: AnyComponent(StoryAvatarInfoComponent( context: context, @@ -199,7 +253,7 @@ public final class StoryContentContextImpl: StoryContentContext { storyItem: mappedItem, isMy: peerId == context.account.peerId ), - totalCount: itemsView.items.count, + totalCount: mappedItems.count, previousItemId: previousItemId, nextItemId: nextItemId ) @@ -840,15 +894,30 @@ public final class SingleStoryContentContextImpl: StoryContentContext { self.storyDisposable = (combineLatest(queue: .mainQueue(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId)), - context.account.postbox.transaction { transaction -> Stories.StoredItem? in - return transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) + context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer]) in + guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else { + return (nil, [:]) + } + var peers: [PeerId: Peer] = [:] + if case let .item(item) = item { + if let views = item.views { + for id in views.seenPeerIds { + if let peer = transaction.getPeer(id) { + peers[peer.id] = peer + } + } + } + } + return (item, peers) } ) - |> deliverOnMainQueue).start(next: { [weak self] peer, item in + |> deliverOnMainQueue).start(next: { [weak self] peer, itemAndPeers in guard let self else { return } + let (item, peers) = itemAndPeers + if item == nil { let storyKey = StoryKey(peerId: storyId.peerId, id: storyId.id) if !self.requestedStoryKeys.contains(storyKey) { @@ -870,14 +939,15 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return EngineStoryItem.Views( seenCount: views.seenCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in - return nil + return peers[id].flatMap(EnginePeer.init) } ) }, privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init), isPinned: itemValue.isPinned, isExpired: itemValue.isExpired, - isPublic: itemValue.isPublic + isPublic: itemValue.isPublic, + isPending: false ) let stateValue = StoryContentContextState( diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index 280d124063..d575b7b4b9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -12,6 +12,7 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import StoryContainerScreen import HierarchyTrackingLayer +import VolumeButtons final class StoryItemContentComponent: Component { typealias EnvironmentType = StoryContentItem.Environment @@ -93,6 +94,8 @@ final class StoryItemContentComponent: Component { private let imageNode: TransformImageNode private var videoNode: UniversalVideoNode? + private var volumeButtonsListener: VolumeButtonsListener? + private var currentMessageMedia: EngineMedia? private var fetchDisposable: Disposable? @@ -141,7 +144,7 @@ final class StoryItemContentComponent: Component { } private func performActionAfterImageContentLoaded(update: Bool) { - guard let component = self.component, let currentMessageMedia = self.currentMessageMedia else { + guard let component = self.component, let environment = self.environment, let currentMessageMedia = self.currentMessageMedia else { return } @@ -160,6 +163,7 @@ final class StoryItemContentComponent: Component { streamVideo: .story, loopVideo: true, enableSound: true, + beginWithAmbientSound: environment.sharedState.useAmbientMode, tempFilePath: nil, captureProtected: false, storeAfterDownload: nil @@ -189,6 +193,19 @@ final class StoryItemContentComponent: Component { if update { self.state?.updated(transition: .immediate) } + + if self.volumeButtonsListener == nil, let sharedState = self.environment?.sharedState, sharedState.useAmbientMode { + self.volumeButtonsListener = VolumeButtonsListener(shouldBeActive: .single(true), valueChanged: { [weak self] in + guard let self, let sharedState = self.environment?.sharedState, sharedState.useAmbientMode else { + return + } + sharedState.useAmbientMode = false + if let videoNode = self.videoNode { + videoNode.continueWithOverridingAmbientMode() + } + self.volumeButtonsListener = nil + }) + } } } } @@ -211,7 +228,14 @@ final class StoryItemContentComponent: Component { private func updateIsProgressPaused() { if let videoNode = self.videoNode { - if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy { + var canPlay = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy + if let component = self.component { + if component.item.isPending { + canPlay = false + } + } + + if canPlay { videoNode.play() } else { videoNode.pause() @@ -223,7 +247,12 @@ final class StoryItemContentComponent: Component { } private func updateProgressTimer() { - let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy + var needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy + if let component = self.component { + if component.item.isPending { + needsTimer = false + } + } if needsTimer { if self.currentProgressTimer == nil { @@ -245,8 +274,8 @@ final class StoryItemContentComponent: Component { } } - #if DEBUG && false - let currentProgressTimerLimit: Double = 1 * 60.0 + #if DEBUG && true + let currentProgressTimerLimit: Double = 10.0 #else let currentProgressTimerLimit: Double = 5.0 #endif diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD index 08439b34a2..c37647c1e2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/AccountContext", "//submodules/TelegramCore", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/SemanticStatusNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 4754c71da0..d8be71f1ca 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -8,6 +8,8 @@ import AnimatedAvatarSetNode import AccountContext import TelegramCore import MoreHeaderButton +import SemanticStatusNode +import SwiftSignalKit public final class StoryFooterPanelComponent: Component { public let context: AccountContext @@ -53,12 +55,19 @@ public final class StoryFooterPanelComponent: Component { private let deleteButton = ComponentView() private var moreButton: MoreHeaderButton? + private var statusButton: HighlightableButton? + private var statusNode: SemanticStatusNode? + private var uploadingText: ComponentView? + private let avatarsContext: AnimatedAvatarSetContext private let avatarsNode: AnimatedAvatarSetNode private var component: StoryFooterPanelComponent? private weak var state: EmptyComponentState? + private var uploadProgress: Float = 0.0 + private var uploadProgressDisposable: Disposable? + override init(frame: CGRect) { self.viewStatsButton = HighlightableButton() @@ -78,6 +87,10 @@ public final class StoryFooterPanelComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.uploadProgressDisposable?.dispose() + } + @objc private func viewStatsPressed() { guard let component = self.component else { return @@ -85,7 +98,37 @@ public final class StoryFooterPanelComponent: Component { component.expandViewStats() } + @objc private func statusPressed() { + guard let component = self.component else { + return + } + guard let storyItem = component.storyItem else { + return + } + component.context.engine.messages.cancelStoryUpload(stableId: storyItem.id) + } + func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.storyItem?.id != component.storyItem?.id || self.component?.storyItem?.isPending != component.storyItem?.isPending { + self.uploadProgressDisposable?.dispose() + self.uploadProgress = 0.0 + + if let storyItem = component.storyItem, storyItem.isPending { + var applyState = false + self.uploadProgressDisposable = (component.context.engine.messages.storyUploadProgress(stableId: storyItem.id) + |> deliverOnMainQueue).start(next: { [weak self] progress in + guard let self else { + return + } + self.uploadProgress = progress + if applyState { + self.state?.updated(transition: .immediate) + } + }) + applyState = true + } + } + self.component = component self.state = state @@ -96,6 +139,84 @@ public final class StoryFooterPanelComponent: Component { let avatarSpacing: CGFloat = 18.0 + let avatarsAlpha: CGFloat + let baseViewCountAlpha: CGFloat + if let storyItem = component.storyItem, storyItem.isPending { + baseViewCountAlpha = 0.0 + + let statusButton: HighlightableButton + if let current = self.statusButton { + statusButton = current + } else { + statusButton = HighlightableButton() + statusButton.addTarget(self, action: #selector(self.statusPressed), for: .touchUpInside) + self.statusButton = statusButton + self.addSubview(statusButton) + } + + let statusNode: SemanticStatusNode + if let current = self.statusNode { + statusNode = current + } else { + statusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white, image: nil, overlayForegroundNodeColor: nil, cutout: nil) + self.statusNode = statusNode + statusButton.addSubview(statusNode.view) + } + + let uploadingText: ComponentView + if let current = self.uploadingText { + uploadingText = current + } else { + uploadingText = ComponentView() + self.uploadingText = uploadingText + } + + var innerLeftOffset: CGFloat = 0.0 + + let statusSize = CGSize(width: 36.0, height: 36.0) + statusNode.view.frame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - statusSize.height) * 0.5)), size: statusSize) + innerLeftOffset += statusSize.width + 10.0 + + statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0))) + + //TODO:localize + let uploadingTextSize = uploadingText.update( + transition: .immediate, + component: AnyComponent(Text(text: "Uploading...", font: Font.regular(15.0), color: .white)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + let uploadingTextFrame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - uploadingTextSize.height) * 0.5)), size: uploadingTextSize) + if let uploadingTextView = uploadingText.view { + if uploadingTextView.superview == nil { + statusButton.addSubview(uploadingTextView) + } + uploadingTextView.frame = uploadingTextFrame + } + innerLeftOffset += uploadingTextSize.width + 8.0 + + transition.setFrame(view: statusButton, frame: CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: CGSize(width: innerLeftOffset, height: size.height))) + leftOffset += innerLeftOffset + + avatarsAlpha = 0.0 + } else { + if let statusNode = self.statusNode { + self.statusNode = nil + statusNode.view.removeFromSuperview() + } + if let uploadingText = self.uploadingText { + self.uploadingText = nil + uploadingText.view?.removeFromSuperview() + } + if let statusButton = self.statusButton { + self.statusButton = nil + statusButton.removeFromSuperview() + } + + avatarsAlpha = pow(1.0 - component.expandFraction, 1.0) + baseViewCountAlpha = 1.0 + } + var peers: [EnginePeer] = [] if let seenPeers = component.storyItem?.views?.seenPeers { peers = Array(seenPeers.prefix(3)) @@ -105,8 +226,7 @@ public final class StoryFooterPanelComponent: Component { let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) self.avatarsNode.frame = avatarsNodeFrame - //transition.setScale(view: self.avatarsNode.view, scale: CGFloat(1.0).interpolate(to: 0.001, amount: component.expandFraction)) - transition.setAlpha(view: self.avatarsNode.view, alpha: pow(1.0 - component.expandFraction, 1.0)) + transition.setAlpha(view: self.avatarsNode.view, alpha: avatarsAlpha) if !avatarsSize.width.isZero { leftOffset = avatarsNodeFrame.maxX + avatarSpacing } @@ -154,7 +274,7 @@ public final class StoryFooterPanelComponent: Component { } transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.center) transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size)) - transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2)) + transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2) * baseViewCountAlpha) transition.setScale(view: viewStatsTextView, scale: viewStatsCurrentFrame.width / viewStatsTextFrame.width) } @@ -166,7 +286,7 @@ public final class StoryFooterPanelComponent: Component { } transition.setPosition(view: viewStatsExpandedTextView, position: viewStatsExpandedTextFrame.center) transition.setBounds(view: viewStatsExpandedTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsExpandedTextFrame.size)) - transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2)) + transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2) * baseViewCountAlpha) transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width) } @@ -199,7 +319,7 @@ public final class StoryFooterPanelComponent: Component { transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize)) rightContentOffset -= deleteButtonSize.width + 8.0 - transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0)) + transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha) } let moreButton: MoreHeaderButton @@ -235,7 +355,7 @@ public final class StoryFooterPanelComponent: Component { let buttonSize = CGSize(width: 32.0, height: 44.0) moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)) - transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0)) + transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha) return size } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 1fc86815eb..544db58957 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -23,6 +23,7 @@ public final class StoryPeerListComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings + public let sideInset: CGFloat public let includesHidden: Bool public let storySubscriptions: EngineStorySubscriptions? public let collapseFraction: CGFloat @@ -36,6 +37,7 @@ public final class StoryPeerListComponent: Component { context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + sideInset: CGFloat, includesHidden: Bool, storySubscriptions: EngineStorySubscriptions?, collapseFraction: CGFloat, @@ -48,6 +50,7 @@ public final class StoryPeerListComponent: Component { self.context = context self.theme = theme self.strings = strings + self.sideInset = sideInset self.includesHidden = includesHidden self.storySubscriptions = storySubscriptions self.collapseFraction = collapseFraction @@ -67,6 +70,9 @@ public final class StoryPeerListComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.sideInset != rhs.sideInset { + return false + } if lhs.includesHidden != rhs.includesHidden { return false } @@ -529,7 +535,7 @@ public final class StoryPeerListComponent: Component { let itemLayout = ItemLayout( containerSize: availableSize, - containerInsets: UIEdgeInsets(top: 4.0, left: 10.0, bottom: 0.0, right: 10.0), + containerInsets: UIEdgeInsets(top: 4.0, left: component.sideInset - 4.0, bottom: 0.0, right: component.sideInset - 4.0), itemSize: CGSize(width: 60.0, height: 77.0), itemSpacing: 24.0, itemCount: self.sortedItems.count diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 3c3b9830d0..86a425dbd0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -708,7 +708,7 @@ public final class StoryPeerListItemComponent: Component { } let titleSize = self.title.update( - transition: titleTransition, + transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)), maximumNumberOfLines: 1 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 71c75df56b..5103a5ef03 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -97,6 +97,7 @@ import LegacyInstantVideoController import StoryContainerScreen import StoryContentComponent import MoreHeaderButton +import VolumeButtons #if DEBUG import os.signpost @@ -4532,7 +4533,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transitionIn = StoryContainerScreen.TransitionIn( sourceView: result, sourceRect: result.bounds, - sourceCornerRadius: 2.0 + sourceCornerRadius: 2.0, + sourceIsAvatar: false ) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 76fafa5c08..b3fc60d7ae 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -1180,6 +1180,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) + } else if let contentImageNode = self.contentImageNode, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in + return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) + }) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 68434232f4..9659de86ed 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -103,7 +103,7 @@ private enum ChatListSearchEntry: Comparable, Identifiable { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - hasNewStories: false + storyState: nil )), editing: false, hasActiveRevealControls: false, @@ -268,7 +268,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openPremiumIntro: { }, openChatFolderUpdates: { }, hideChatFolderUpdates: { - }, openStories: { _ in + }, openStories: { _, _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index bfe12290c5..45887387ca 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -157,9 +157,15 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam resolvedKeepStack = false } if resolvedKeepStack { - params.navigationController.pushViewController(controller, animated: params.animated, completion: { - params.completion(controller) - }) + if let pushController = params.pushController { + pushController(controller, params.animated, { + params.completion(controller) + }) + } else { + params.navigationController.pushViewController(controller, animated: params.animated, completion: { + params.completion(controller) + }) + } } else { let viewControllers = params.navigationController.viewControllers.filter({ controller in if controller is ForumCreateTopicScreen { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 39dfe3d449..cac2031ec8 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -42,14 +42,67 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak navigationController] _ in - let transitionIn: StoryContainerScreen.TransitionIn? = nil + var transitionIn: StoryContainerScreen.TransitionIn? = nil + + var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + selectedTransitionNode = params.transitionNode(params.message.id, story) + + if let selectedTransitionNode { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: selectedTransitionNode.0.view, + sourceRect: selectedTransitionNode.1, + sourceCornerRadius: 0.0, + sourceIsAvatar: false + ) + } + + let hiddenMediaSource = params.context.sharedContext.mediaManager.galleryHiddenMediaManager.addSource(.single(GalleryHiddenMediaId.chat(params.context.account.id, params.message.id, story))) let storyContainerScreen = StoryContainerScreen( context: context, content: storyContent, transitionIn: transitionIn, transitionOut: { _, _ in - let transitionOut: StoryContainerScreen.TransitionOut? = nil + var transitionOut: StoryContainerScreen.TransitionOut? = nil + + var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + selectedTransitionNode = params.transitionNode(params.message.id, story) + if let selectedTransitionNode { + transitionOut = StoryContainerScreen.TransitionOut( + destinationView: selectedTransitionNode.0.view, + transitionView: StoryContainerScreen.TransitionView( + makeView: { + let view = UIView() + if let transitionView = selectedTransitionNode.2().0 { + transitionView.layer.anchorPoint = CGPoint() + view.addSubview(transitionView) + } + return view + }, + updateView: { view, state, transition in + guard let view = view.subviews.first else { + return + } + if state.progress == 0.0 { + view.frame = CGRect(origin: CGPoint(), size: state.sourceSize) + } + + let toScale = state.sourceSize.width / state.destinationSize.width + let fromScale: CGFloat = 1.0 + let scale = toScale.interpolate(to: fromScale, amount: state.progress) + transition.setTransform(view: view, transform: CATransform3DMakeScale(scale, scale, 1.0)) + } + ), + destinationRect: selectedTransitionNode.1, + destinationCornerRadius: 0.0, + destinationIsAvatar: false, + completed: { + params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource) + } + ) + } else { + params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource) + } return transitionOut } diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift index 6f9ada6896..4e5a7ea0e4 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift @@ -115,6 +115,9 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode { self.videoNode.playOnceWithSound(playAndRecord: playAndRecord) } + func continueWithOverridingAmbientMode() { + } + func pause() { self.videoNode.pause() } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index c91f293386..780f5947c4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -467,7 +467,9 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { transition: Transition(transition), component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: hasUnseenStories, - isDarkTheme: theme.overallDarkAppearance + isDarkTheme: theme.overallDarkAppearance, + activeLineWidth: 3.0, + inactiveLineWidth: 2.0 )), environment: {}, containerSize: self.avatarNode.bounds.size diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index be5be12c0a..88fc0a5e4e 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4108,7 +4108,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro transitionIn = StoryContainerScreen.TransitionIn( sourceView: transitionView, sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 + sourceCornerRadius: transitionView.bounds.height * 0.5, + sourceIsAvatar: true ) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index c2a37e3a55..6ab036859f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -348,8 +348,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let imageData = compressImageToJPEG(image, quality: 0.6) { switch privacy { case let .story(storyPrivacy, period, pin): - chatListController.updateStoryUploadProgress(0.0) - let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) + self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) + + /*let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) |> deliverOnMainQueue).start(next: { [weak chatListController] result in if let chatListController { switch result { @@ -364,7 +365,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } } - }) + })*/ Queue.mainQueue().justDispatch { commit({}) } @@ -420,7 +421,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon commit({}) } } - case let .video(content, _, values, duration, dimensions, caption): + case let .video(content, firstFrameImage, values, duration, dimensions, caption): let adjustments: VideoMediaResourceAdjustments if let valuesData = try? JSONEncoder().encode(values) { let data = MemoryBuffer(data: valuesData) @@ -436,9 +437,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .asset(localIdentifier): resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } + + let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } + if case let .story(storyPrivacy, period, pin) = privacy { - chatListController.updateStoryUploadProgress(0.0) - let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) + self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) + /*let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId) |> deliverOnMainQueue).start(next: { [weak chatListController] result in if let chatListController { switch result { @@ -453,7 +457,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } } - }) + })*/ Queue.mainQueue().justDispatch { commit({}) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 1aa79e23d0..68424ec9a0 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -36,6 +36,7 @@ public final class NativeVideoContent: UniversalVideoContent { public let streamVideo: MediaPlayerStreaming public let loopVideo: Bool public let enableSound: Bool + public let beginWithAmbientSound: Bool public let baseRate: Double let fetchAutomatically: Bool let onlyFullSizeThumbnail: Bool @@ -51,7 +52,7 @@ public final class NativeVideoContent: UniversalVideoContent { let hintDimensions: CGSize? let storeAfterDownload: (() -> Void)? - public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?) { + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, beginWithAmbientSound: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?) { self.id = id self.nativeId = id self.userLocation = userLocation @@ -74,6 +75,7 @@ public final class NativeVideoContent: UniversalVideoContent { self.streamVideo = streamVideo self.loopVideo = loopVideo self.enableSound = enableSound + self.beginWithAmbientSound = beginWithAmbientSound self.baseRate = baseRate self.fetchAutomatically = fetchAutomatically self.onlyFullSizeThumbnail = onlyFullSizeThumbnail @@ -91,7 +93,7 @@ public final class NativeVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, beginWithAmbientSound: self.beginWithAmbientSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -113,6 +115,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference private let enableSound: Bool + private let beginWithAmbientSound: Bool private let loopVideo: Bool private let baseRate: Double private let audioSessionManager: ManagedAudioSession @@ -167,12 +170,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var shouldPlay: Bool = false - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, beginWithAmbientSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference self.placeholderColor = placeholderColor self.enableSound = enableSound + self.beginWithAmbientSound = beginWithAmbientSound self.loopVideo = loopVideo self.baseRate = baseRate self.audioSessionManager = audioSessionManager @@ -181,7 +185,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.imageNode = TransformImageNode() - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, ambient: beginWithAmbientSound, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -439,6 +443,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.setForceAudioToSpeaker(forceAudioToSpeaker) } + func continueWithOverridingAmbientMode() { + self.player.continueWithOverridingAmbientMode() + } + func setBaseRate(_ baseRate: Double) { self.player.setBaseRate(baseRate) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 948c6d6d9e..76ead9b753 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -430,6 +430,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } + func continueWithOverridingAmbientMode() { + } + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 25e63126ee..ddbf795d69 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -267,6 +267,9 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } + func continueWithOverridingAmbientMode() { + } + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 5bb66abdc5..5fa917db02 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -164,6 +164,9 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { } } + func continueWithOverridingAmbientMode() { + } + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } diff --git a/submodules/Utils/VolumeButtons/BUILD b/submodules/Utils/VolumeButtons/BUILD new file mode 100644 index 0000000000..faffc51c8d --- /dev/null +++ b/submodules/Utils/VolumeButtons/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "VolumeButtons", + module_name = "VolumeButtons", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/LegacyComponents", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/VolumeButtons.swift b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift similarity index 86% rename from submodules/TelegramUI/Sources/VolumeButtons.swift rename to submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift index 87f905d543..693235f365 100644 --- a/submodules/TelegramUI/Sources/VolumeButtons.swift +++ b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift @@ -5,12 +5,12 @@ import MediaPlayer import LegacyComponents -class VolumeButtonsListener: NSObject { +public class VolumeButtonsListener: NSObject { private let handler: PGCameraVolumeButtonHandler private var disposable: Disposable? - init(shouldBeActive: Signal, valueChanged: @escaping () -> Void) { + public init(shouldBeActive: Signal, valueChanged: @escaping () -> Void) { var impl: (() -> Void)? self.handler = PGCameraVolumeButtonHandler(upButtonPressedBlock: {