diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index f6c445ac6c..d7f0ccc244 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -649,7 +649,7 @@ public final class PeerInfoNavigationSourceTag { } public protocol PeerInfoScreen: ViewController { - + var peerId: PeerId { get } } public protocol ChatController: ViewController { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5242db897b..bbbebc5b34 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2177,7 +2177,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) #endif - if let lockViewFrame = self.findTitleView()?.lockViewFrame, !self.didShowPasscodeLockTooltipController { + if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), let _ = storyPeerListView.lockViewFrame(), !self.didShowPasscodeLockTooltipController { self.passcodeLockTooltipDisposable.set(combineLatest(queue: .mainQueue(), ApplicationSpecificNotice.getPasscodeLockTips(accountManager: self.context.sharedContext.accountManager), self.context.sharedContext.accountManager.accessChallengeData() |> take(1)).start(next: { [weak self] tooltipValue, passcodeView in if let strongSelf = self { if !tooltipValue { @@ -2187,8 +2187,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.DialogList_PasscodeLockHelp), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in - if let strongSelf = self, let titleView = strongSelf.findTitleView() { - return (titleView, lockViewFrame.offsetBy(dx: 4.0, dy: 14.0)) + if let self, let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), let lockViewFrame = storyPeerListView.lockViewFrame() { + return (storyPeerListView, lockViewFrame.offsetBy(dx: 4.0, dy: 14.0)) } return nil })) @@ -5087,8 +5087,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } public var lockViewFrame: CGRect? { - if let titleView = self.findTitleView(), let lockViewFrame = titleView.lockViewFrame { - return titleView.convert(lockViewFrame, to: self.view) + if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView(), let lockViewFrame = storyPeerListView.lockViewFrame() { + return storyPeerListView.convert(lockViewFrame, to: self.view) } else { return nil } @@ -5465,7 +5465,35 @@ private final class ChatListLocationContext { #elseif DEBUG && false networkState = .single(AccountNetworkState.connecting(proxy: nil)) #else - networkState = context.account.networkState + let realNetworkState = context.account.networkState + networkState = Signal { subscriber in + let currentValue = Atomic(value: nil) + let disposable = (realNetworkState + |> mapToSignal { value -> Signal in + let previousValue = currentValue.swap(value) + if let previousValue { + switch value { + case .waitingForNetwork, .connecting, .updating: + if case .online = previousValue { + return .single(value) |> delay(0.3, queue: .mainQueue()) + } else { + return .single(value) + } + default: + return .single(value) + } + } else { + return .single(value) + } + } + |> deliverOnMainQueue).start(next: { value in + subscriber.putNext(value) + }) + + return ActionDisposable { + disposable.dispose() + } + } #endif switch location { diff --git a/submodules/Display/Source/HighlightTrackingButton.swift b/submodules/Display/Source/HighlightTrackingButton.swift index c48a9b2b33..0a97cd97a7 100644 --- a/submodules/Display/Source/HighlightTrackingButton.swift +++ b/submodules/Display/Source/HighlightTrackingButton.swift @@ -6,6 +6,14 @@ open class HighlightTrackingButton: UIButton { public var internalHighligthedChanged: (Bool) -> Void = { _ in } public var highligthedChanged: (Bool) -> Void = { _ in } + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { if !self.internalHighlighted { self.internalHighlighted = true diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index dc1c0b17bc..d55566ef4c 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -1148,7 +1148,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textBackgroundNode.isHidden = true - if let accessoryComponentView = node.accessoryComponentView { + /*if let accessoryComponentView = node.accessoryComponentView { let tempContainer = UIView() let accessorySize = accessoryComponentView.bounds.size @@ -1161,14 +1161,15 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) tempContainer.addSubview(accessoryComponentView) self.view.addSubview(tempContainer) - } + }*/ self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak node] _ in textBackgroundCompleted = true intermediateCompletion() - if let node = node, let accessoryComponentContainer = node.accessoryComponentContainer, let accessoryComponentView = node.accessoryComponentView { - accessoryComponentContainer.addSubview(accessoryComponentView) + if let node = node, let accessoryComponentView = node.accessoryComponentView { + //accessoryComponentContainer.addSubview(accessoryComponentView) + accessoryComponentView.layer.animateAlpha(from: 0.0, to: accessoryComponentView.alpha, duration: 0.2) } }) diff --git a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index 1a22608d1f..25c468e6e6 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -246,7 +246,9 @@ public class SearchBarPlaceholderNode: ASDisplayNode { } if let accessoryComponentContainer = strongSelf.accessoryComponentContainer { - accessoryComponentContainer.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentContainer.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentContainer.bounds.height) / 2.0)), size: accessoryComponentContainer.bounds.size) + accessoryComponentContainer.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentContainer.bounds.width - 4.0, y: floor((constrainedSize.height * expansionProgress - accessoryComponentContainer.bounds.height) / 2.0)), size: accessoryComponentContainer.bounds.size) + transition.updateAlpha(layer: accessoryComponentContainer.layer, alpha: innerAlpha) + } } }) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 83d34f3138..284a267ca9 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1157,6 +1157,7 @@ public class Account { self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).start()) self.managedOperationsDisposable.add(managedAutoexpireStoryOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start()) + self.managedOperationsDisposable.add(managedSynchronizeViewStoriesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) let extractedExpr: [Signal] = [ diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 725b68f8a5..1e47a0d370 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -196,6 +196,7 @@ private var declaredEncodables: Void = { declareEncodable(TranslationMessageAttribute.self, f: { TranslationMessageAttribute(decoder: $0) }) declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) }) declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) }) + declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeViewStoriesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeViewStoriesOperations.swift new file mode 100644 index 0000000000..6250ec7405 --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeViewStoriesOperations.swift @@ -0,0 +1,132 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +private final class ManagedSynchronizeViewStoriesOperationsHelper { + var operationDisposables: [PeerId: (Int32, Disposable)] = [:] + + func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = [] + + var validPeerIds: [PeerId] = [] + for entry in entries { + guard let operation = entry.contents as? SynchronizeViewStoriesOperation else { + continue + } + validPeerIds.append(entry.peerId) + var replace = true + if let current = self.operationDisposables[entry.peerId] { + if current.0 != operation.storyId { + disposeOperations.append(current.1) + replace = true + } + } else { + replace = true + } + if replace { + let disposable = MetaDisposable() + self.operationDisposables[entry.peerId] = (operation.storyId, disposable) + beginOperations.append((entry, disposable)) + } + } + + var removedPeerIds: [PeerId] = [] + for (peerId, info) in self.operationDisposables { + if !validPeerIds.contains(peerId) { + removedPeerIds.append(peerId) + disposeOperations.append(info.1) + } + } + for peerId in removedPeerIds { + self.operationDisposables.removeValue(forKey: peerId) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values.map(\.1)) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PeerMergedOperationLogEntry? + transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizeViewStories, tagLocalIndex: tagLocalIndex, { entry in + if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeViewStoriesOperation { + result = entry.mergedEntry! + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } else { + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } + }) + + return f(transaction, result) + } |> switchToLatest +} + +func managedSynchronizeViewStoriesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedSynchronizeViewStoriesOperationsHelper()) + + let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeViewStories, limit: 10).start(next: { view in + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in + return helper.update(view.entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in + if let entry = entry { + if let operation = entry.contents as? SynchronizeViewStoriesOperation { + if let peer = transaction.getPeer(entry.peerId) { + return pushStoriesAreSeen(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation) + } else { + return .complete() + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then(postbox.transaction { transaction -> Void in + let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizeViewStories, tagLocalIndex: entry.tagLocalIndex) + }) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizeViewStoriesOperation) -> Signal { + guard let inputPeer = apiInputUser(peer) else { + return .complete() + } + return network.request(Api.functions.stories.readStories(userId: inputPeer, maxId: operation.storyId)) + |> `catch` { _ -> Signal<[Int32], NoError> in + return .single([]) + } + |> map { _ -> Void in + return Void() + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index f898c70db9..a9f20587cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -185,6 +185,7 @@ public struct OperationLogTags { public static let SynchronizeMarkAllUnseenReactions = PeerOperationLogTag(value: 21) public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22) public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23) + public static let SynchronizeViewStories = PeerOperationLogTag(value: 24) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift new file mode 100644 index 0000000000..e2d53dc398 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SynchronizeViewStoriesOperation.swift @@ -0,0 +1,46 @@ +import Foundation +import Postbox + +public final class SynchronizeViewStoriesOperation: PostboxCoding { + public let peerId: PeerId + public let storyId: Int32 + + public init(peerId: PeerId, storyId: Int32) { + self.peerId = peerId + self.storyId = storyId + } + + public init(decoder: PostboxDecoder) { + self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0)) + self.storyId = decoder.decodeInt32ForKey("s", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.peerId.toInt64(), forKey: "p") + encoder.encodeInt32(self.storyId, forKey: "s") + } +} + +func _internal_addSynchronizeViewStoriesOperation(peerId: PeerId, storyId: Int32, transaction: Transaction) { + let tag: PeerOperationLogTag = OperationLogTags.SynchronizeViewStories + + var topOperation: (SynchronizeViewStoriesOperation, Int32)? + transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in + if let operation = entry.contents as? SynchronizeViewStoriesOperation { + topOperation = (operation, entry.tagLocalIndex) + } + return false + }) + var replace = false + if let (topOperation, topLocalIndex) = topOperation { + if topOperation.storyId < storyId { + let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex) + } + replace = true + } else { + replace = true + } + if replace { + transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeViewStoriesOperation(peerId: peerId, storyId: storyId)) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 9e074a7733..3accb87b8a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -5,7 +5,7 @@ import TelegramApi public enum EngineStoryInputMedia { case image(dimensions: PixelDimensions, data: Data, stickers: [TelegramMediaFile]) - case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?, stickers: [TelegramMediaFile]) + case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameFile: TempBoxFile?, stickers: [TelegramMediaFile]) var embeddedStickers: [TelegramMediaFile] { switch self { @@ -602,11 +602,10 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput flags: [] ) return imageMedia - case let .video(dimensions, duration, resource, firstFrameImageData, _): + case let .video(dimensions, duration, resource, firstFrameFile, _): 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) + if let firstFrameFile = firstFrameFile { + account.postbox.mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "first-frame", keepDuration: .general, tempFile: firstFrameFile) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } @@ -1147,26 +1146,20 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPi ).postboxRepresentation) } + _internal_addSynchronizeViewStoriesOperation(peerId: peerId, storyId: id, transaction: transaction) + return transaction.getPeer(peerId).flatMap(apiInputUser) } - |> mapToSignal { inputUser -> Signal in - guard let inputUser = inputUser else { - return .complete() - } - + |> mapToSignal { _ -> Signal in account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)]) - #if DEBUG && false - if "".isEmpty { - return .complete() - } - #endif + return .complete() - return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id)) + /*return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id)) |> `catch` { _ -> Signal<[Int32], NoError> in return .single([]) } - |> ignoreValues + |> ignoreValues*/ } } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index c7e9e5a477..4f3410a738 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -952,6 +952,12 @@ public final class ChatListHeaderComponent: Component { return } self.component?.openStatusSetup(sourceView) + }, + lockAction: { [weak self] in + guard let self else { + return + } + self.component?.toggleIsLocked() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index c737f53630..637356d4aa 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -336,7 +336,7 @@ public final class ChatListNavigationBar: Component { storiesUnlocked = false } - if allowAvatarsExpansion && transition.animation.isImmediate { + if allowAvatarsExpansion, transition.animation.isImmediate, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { if self.storiesUnlocked != storiesUnlocked { if storiesUnlocked { HapticFeedback().tap() diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift index 36a7ca93a3..12ea7f3b3f 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift @@ -3,11 +3,13 @@ import UIKit import Display import TelegramPresentationData -final class ChatListTitleLockView: UIView { +public final class ChatListTitleLockView: UIView { private let topView: UIImageView private let bottomView: UIImageView - override init(frame: CGRect) { + private var theme: PresentationTheme? + + override public init(frame: CGRect) { self.topView = UIImageView() self.bottomView = UIImageView() @@ -21,10 +23,14 @@ final class ChatListTitleLockView: UIView { fatalError("init(coder:) has not been implemented") } - func updateTheme(_ theme: PresentationTheme) { - self.topView.image = PresentationResourcesChatList.lockTopUnlockedImage(theme) - self.bottomView.image = PresentationResourcesChatList.lockBottomUnlockedImage(theme) - self.layoutItems() + public func updateTheme(_ theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.topView.image = PresentationResourcesChatList.lockTopUnlockedImage(theme) + self.bottomView.image = PresentationResourcesChatList.lockBottomUnlockedImage(theme) + self.layoutItems() + } } private func layoutItems() { @@ -32,7 +38,7 @@ final class ChatListTitleLockView: UIView { self.bottomView.frame = CGRect(x: 0.0, y: 6.0, width: 10.0, height: 8.0) } - override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() self.layoutItems() diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 283da79577..3dfa3e7ebc 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -177,6 +177,7 @@ public final class PeerListItemComponent: Component { self.separatorLayer = SimpleLayer() self.containerButton = HighlightTrackingButton() + self.containerButton.isExclusiveTouch = true self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = false diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 165db01fe4..09b760c7ea 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1083,6 +1083,9 @@ private final class StoryContainerScreenComponent: Component { if self.isHoldingTouch { isProgressPaused = true } + if !environment.isVisible { + isProgressPaused = true + } var dismissPanOffset: CGFloat = 0.0 var dismissPanScale: CGFloat = 1.0 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 8f4d0de372..666c12deff 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -135,7 +135,7 @@ final class StoryItemContentComponent: Component { manager: component.context.sharedContext.mediaManager.universalVideoManager, decoration: StoryVideoDecoration(), content: NativeVideoContent( - id: .message(0, file.fileId), + id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"), userLocation: .other, fileReference: .story(peer: peerReference, id: component.item.id, media: file), imageReference: nil, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e596a193f4..0793a7b064 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2470,7 +2470,7 @@ public final class StoryItemSetContainerComponent: Component { } })), environment: {}, - containerSize: CGSize(width: contentFrame.width, height: 44.0) + containerSize: CGSize(width: max(10.0, contentFrame.width - headerRightOffset), height: 44.0) ) if let view = currentCenterInfoItem.view.view { var animateIn = false @@ -3194,20 +3194,28 @@ public final class StoryItemSetContainerComponent: Component { } })) } else { - guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { - return - } - - if "".isEmpty { - navigationController.pushViewController(chatController) - } else { - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === controller }) { - viewControllers.insert(chatController, at: index) - } else { - viewControllers.append(chatController) + var currentViewControllers = navigationController.viewControllers + if let index = currentViewControllers.firstIndex(where: { c in + if let c = c as? PeerInfoScreen, c.peerId == peer.id { + return true } - navigationController.setViewControllers(viewControllers, animated: true) + return false + }) { + if let controller = component.controller() as? StoryContainerScreen { + controller.dismiss() + } + for i in (index + 1 ..< currentViewControllers.count).reversed() { + if currentViewControllers[i] !== component.controller() { + currentViewControllers.remove(at: i) + } + } + navigationController.setViewControllers(currentViewControllers, animated: true) + } else { + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + + navigationController.pushViewController(chatController) } } } @@ -3402,7 +3410,16 @@ public final class StoryItemSetContainerComponent: Component { } let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } - let _ = (context.engine.messages.editStory(id: id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: firstFrameImageData, stickers: stickers), text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) + let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in + let file = TempBox.shared.tempFile(fileName: "image.jpg") + if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { + return file + } else { + return nil + } + } + + let _ = (context.engine.messages.editStory(id: id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index 33f6e6a708..6658f5726d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/ChatListTitleView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 286068996b..8a93057711 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -11,32 +11,7 @@ import SwiftSignalKit import TelegramPresentationData import StoryContainerScreen import EmojiStatusComponent - -private func solveParabolicMotion(from sourcePoint: CGPoint, to targetPosition: CGPoint, progress: CGFloat) -> CGPoint { - if sourcePoint.y == targetPosition.y { - return sourcePoint.interpolate(to: targetPosition, amount: progress) - } - - //(x - h)² + (y - k)² = r² - //(x1 - h) * (x1 - h) + (y1 - k) * (y1 - k) = r * r - //(x2 - h) * (x2 - h) + (y2 - k) * (y2 - k) = r * r - - let x1 = sourcePoint.y - let y1 = sourcePoint.x - let x2 = targetPosition.y - let y2 = targetPosition.x - - let b = (x1 * x1 * y2 - x2 * x2 * y1) / (x1 * x1 - x2 * x2) - let k = (y1 - y2) / (x1 * x1 - x2 * x2) - - let x = sourcePoint.y.interpolate(to: targetPosition.y, amount: progress) - let y = k * x * x + b - return CGPoint(x: y, y: x) -} - -private let modelSpringAnimation: CABasicAnimation = { - return makeSpringBounceAnimation("", 0.0, 88.0) -}() +import ChatListTitleView public final class StoryPeerListComponent: Component { public enum PeerStatus: Equatable { @@ -84,6 +59,7 @@ public final class StoryPeerListComponent: Component { public let peerAction: (EnginePeer?) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void public let openStatusSetup: (UIView) -> Void + public let lockAction: () -> Void public init( externalState: ExternalState, @@ -104,7 +80,8 @@ public final class StoryPeerListComponent: Component { uploadProgress: Float?, peerAction: @escaping (EnginePeer?) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, - openStatusSetup: @escaping (UIView) -> Void + openStatusSetup: @escaping (UIView) -> Void, + lockAction: @escaping () -> Void ) { self.externalState = externalState self.context = context @@ -125,6 +102,7 @@ public final class StoryPeerListComponent: Component { self.peerAction = peerAction self.contextPeerAction = contextPeerAction self.openStatusSetup = openStatusSetup + self.lockAction = lockAction } public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { @@ -217,14 +195,17 @@ public final class StoryPeerListComponent: Component { func frame(at index: Int) -> CGRect { if self.itemCount <= 1 { - return CGRect(origin: CGPoint(x: floor((self.containerSize.width - self.itemSize.width) * 0.5), y: self.containerInsets.top), size: self.itemSize) + return CGRect(origin: CGPoint(x: self.containerInsets.left + floor((self.containerSize.width - self.containerInsets.left - containerInsets.right - self.itemSize.width) * 0.5), y: self.containerInsets.top), size: self.itemSize) } else if self.contentSize.width < self.containerSize.width { let usableWidth = self.containerSize.width - self.containerInsets.left - self.containerInsets.right let usableSpacingWidth = usableWidth - self.itemSize.width * CGFloat(self.itemCount) var spacing = floor(usableSpacingWidth / CGFloat(self.itemCount + 1)) - spacing = min(120.0, spacing) - return CGRect(origin: CGPoint(x: self.containerInsets.left + spacing + (self.itemSize.width + spacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) + spacing = min(100.0, spacing) + + let contentWidth = self.itemSize.width * CGFloat(self.itemCount) + spacing * CGFloat(max(0, self.itemCount - 1)) + + return CGRect(origin: CGPoint(x: floor((self.containerSize.width - contentWidth) * 0.5) + (self.itemSize.width + spacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) } else { return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) } @@ -338,6 +319,8 @@ public final class StoryPeerListComponent: Component { private var titleIndicatorView: ComponentView? + private var titleLockView: ChatListTitleLockView? + private var titleLockButton: HighlightTrackingButton? private let titleView: UIImageView private var titleState: TitleState? private var titleViewAnimation: TitleAnimationState? @@ -429,6 +412,15 @@ public final class StoryPeerListComponent: Component { component.peerAction(nil) } + @objc private func titleLockButtonPressed() { + guard let component = self.component else { + return + } + if component.titleHasLock { + component.lockAction() + } + } + public func setPreviewedItem(signal: Signal) { self.previewedItemDisposable?.dispose() self.previewedItemDisposable = (signal |> map(\.?.peerId) |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] itemId in @@ -460,6 +452,14 @@ public final class StoryPeerListComponent: Component { return self.titleView.frame } + public func lockViewFrame() -> CGRect? { + if let titleLockView = self.titleLockView { + return titleLockView.frame + } else { + return nil + } + } + public func transitionViewForItem(peerId: EnginePeer.Id) -> (UIView, StoryContainerScreen.TransitionView)? { if self.collapsedButton.isUserInteractionEnabled { return nil @@ -507,6 +507,12 @@ public final class StoryPeerListComponent: Component { let titleSize = self.titleView.image?.size ?? CGSize() realTitleContentWidth += titleSize.width + var titleLockOffset: CGFloat = 0.0 + if component.titleHasLock { + titleLockOffset = 20.0 + } + realTitleContentWidth += titleLockOffset + var titleIconSize: CGSize? if let peerStatus = component.titlePeerStatus { let statusContent: EmojiStatusComponent.Content @@ -911,13 +917,8 @@ public final class StoryPeerListComponent: Component { let totalCount: Int let unseenCount: Int - if peer.id == component.context.account.peerId { - totalCount = 1 - unseenCount = itemSet.unseenCount != 0 ? 1 : 0 - } else { - totalCount = itemSet.storyCount - unseenCount = itemSet.unseenCount - } + totalCount = itemSet.storyCount + unseenCount = itemSet.unseenCount let _ = visibleItem.view.update( transition: itemTransition, @@ -1192,7 +1193,7 @@ public final class StoryPeerListComponent: Component { titleIndicatorView.alpha = indicatorAlpha } - let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 2.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset + titleLockOffset, y: collapsedItemOffsetY + 2.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize) if let image = self.titleView.image { self.titleView.center = CGPoint(x: titleFrame.minX, y: titleFrame.midY) self.titleView.bounds = CGRect(origin: CGPoint(), size: image.size) @@ -1210,6 +1211,38 @@ public final class StoryPeerListComponent: Component { self.titleView.layer.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0) } + if component.titleHasLock { + let titleLockView: ChatListTitleLockView + if let current = self.titleLockView { + titleLockView = current + } else { + titleLockView = ChatListTitleLockView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))) + self.titleLockView = titleLockView + self.addSubview(titleLockView) + } + titleLockView.updateTheme(component.theme) + + let lockFrame = CGRect(x: titleFrame.minX - 6.0 - 12.0, y: titleFrame.minY + 3.0, width: 2.0, height: 2.0) + titleLockView.frame = lockFrame + + let titleLockButton: HighlightTrackingButton + if let current = self.titleLockButton { + titleLockButton = current + } else { + titleLockButton = HighlightTrackingButton() + self.titleLockButton = titleLockButton + self.addSubview(titleLockButton) + titleLockButton.addTarget(self, action: #selector(self.titleLockButtonPressed), for: .touchUpInside) + } + titleLockButton.frame = CGRect(origin: CGPoint(x: lockFrame.minX - 4.0, y: titleFrame.minY - 4.0), size: CGSize(width: titleFrame.maxX - lockFrame.minX + 4.0, height: titleFrame.height + 8.0)) + } else if let titleLockView = self.titleLockView { + self.titleLockView = nil + titleLockView.removeFromSuperview() + + self.titleLockButton?.removeFromSuperview() + self.titleLockButton = nil + } + for disappearingTitleView in self.disappearingTitleViews { if let image = disappearingTitleView.imageView.image { disappearingTitleView.imageView.center = CGPoint(x: titleFrame.minX, y: titleFrame.midY) @@ -1243,6 +1276,8 @@ public final class StoryPeerListComponent: Component { } titleContentOffset += collapsedState.titleWidth + + self.scrollContainerView.isUserInteractionEnabled = collapsedState.maxFraction >= 1.0 - 0.001 } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1256,6 +1291,12 @@ public final class StoryPeerListComponent: Component { } } + if let titleLockButton = self.titleLockButton { + if let result = titleLockButton.hitTest(self.convert(point, to: titleLockButton), with: event) { + return result + } + } + var result: UIView? for view in self.subviews.reversed() { if let resultValue = view.hitTest(self.convert(point, to: view), with: event), resultValue.isUserInteractionEnabled { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 8f1e8d8fea..7254df7504 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -576,14 +576,6 @@ public final class StoryPeerListItemComponent: Component { func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let size = availableSize - transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) - transition.setFrame(view: self.extractedBackgroundView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0)) - - self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height)) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - let themeUpdated = self.component?.theme !== component.theme let previousComponent = self.component @@ -877,6 +869,15 @@ public final class StoryPeerListItemComponent: Component { } } + let extractedBackgroundWidth = max(size.width + 8.0, titleSize.width + 10.0) + transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.extractedBackgroundView, frame: CGRect(origin: CGPoint(x: floor((size.width - extractedBackgroundWidth) * 0.5), y: -4.0), size: CGSize(width: extractedBackgroundWidth, height: size.height + 8.0))) + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + return availableSize } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index e3386052d3..60975f5cfd 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -9834,7 +9834,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder { private let context: AccountContext fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? - private let peerId: PeerId + public let peerId: PeerId private let avatarInitiallyExpanded: Bool private let isOpenedFromChat: Bool private let nearbyPeerDistance: Int32? diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index e02fe26bdf..bde99851eb 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -396,8 +396,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } + let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in + let file = TempBox.shared.tempFile(fileName: "image.jpg") + if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { + return file + } else { + return nil + } + } + let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData, stickers: stickers), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) }