diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index c64754cb48..a3667d4d3e 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -301,15 +301,43 @@ private func testAvatarImage(size: CGSize) -> UIImage? { return image } -private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { +private func avatarRoundImage(size: CGSize, source: UIImage, isStory: Bool) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) let context = UIGraphicsGetCurrentContext() - context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context?.clip() - - source.draw(in: CGRect(origin: CGPoint(), size: size)) + if isStory { + let lineWidth: CGFloat = 2.0 + context?.beginPath() + context?.addEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context?.clip() + + let colors: [CGColor] = [ + UIColor(rgb: 0x34C76F).cgColor, + UIColor(rgb: 0x3DA1FD).cgColor + ] + var locations: [CGFloat] = [0.0, 1.0] + + 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()) + + context?.setBlendMode(.copy) + context?.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 2.0, dy: 2.0)) + + context?.setBlendMode(.normal) + context?.beginPath() + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height).insetBy(dx: 4.0, dy: 4.0)) + context?.clip() + + source.draw(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 4.0, dy: 4.0)) + } else { + context?.beginPath() + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + context?.clip() + + source.draw(in: CGRect(origin: CGPoint(), size: size)) + } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() @@ -332,12 +360,16 @@ private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] -private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [String]) -> UIImage? { +private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [String], isStory: Bool) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 2.0) let context = UIGraphicsGetCurrentContext() context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + if isStory { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height).insetBy(dx: 4.0, dy: 4.0)) + } else { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + } context?.clip() let colorIndex: Int @@ -373,17 +405,38 @@ private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [Stri CTLineDraw(line, context) } context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + + if isStory { + context?.resetClip() + + let lineWidth: CGFloat = 2.0 + context?.setLineWidth(lineWidth) + context?.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height)).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context?.replacePathWithStrokedPath() + context?.clip() + + let colors: [CGColor] = [ + UIColor(rgb: 0x34C76F).cgColor, + UIColor(rgb: 0x3DA1FD).cgColor + ] + var locations: [CGFloat] = [0.0, 1.0] + + 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()) + } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } -private func avatarImage(path: String?, peerId: PeerId, letters: [String], size: CGSize) -> UIImage { - if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { +private func avatarImage(path: String?, peerId: PeerId, letters: [String], size: CGSize, isStory: Bool) -> UIImage { + if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image, isStory: isStory) { return roundImage } else { - return avatarViewLettersImage(size: size, peerId: peerId, letters: letters)! + return avatarViewLettersImage(size: size, peerId: peerId, letters: letters, isStory: isStory)! } } @@ -402,14 +455,15 @@ private func storeTemporaryImage(path: String) -> String { } @available(iOS 15.0, *) -private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) -> INImage? { +private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, isStory: Bool) -> INImage? { if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) { - let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents.png", keepDuration: .shortLived) - if let _ = fileSize(cachedPath) { + let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents\(isStory ? "-story2" : "").png", keepDuration: .shortLived) + if let _ = fileSize(cachedPath), !"".isEmpty { return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { - let image = avatarImage(path: path, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) + let image = avatarImage(path: path, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0), isStory: isStory) if let data = image.pngData() { + let _ = try? FileManager.default.removeItem(atPath: cachedPath) let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } @@ -417,11 +471,11 @@ private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) - } } - let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar2-\(peer.displayLetters.joined(separator: ","))", representationId: "intents.png", keepDuration: .shortLived) + let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar2-\(peer.displayLetters.joined(separator: ","))\(isStory ? "-story" : "")", representationId: "intents.png", keepDuration: .shortLived) if let _ = fileSize(cachedPath) { return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { - let image = avatarImage(path: nil, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) + let image = avatarImage(path: nil, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0), isStory: isStory) if let data = image.pngData() { let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } @@ -468,9 +522,9 @@ private struct NotificationContent: CustomStringConvertible { return string } - mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, topicTitle: String?, contactIdentifier: String?) { + mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, topicTitle: String?, contactIdentifier: String?, isStory: Bool) { if #available(iOS 15.0, *) { - let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer) + let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer, isStory: isStory) self.senderImage = image @@ -1527,7 +1581,7 @@ private final class NotificationServiceHandler { return true }) - content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId) + content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId, isStory: false) } } @@ -1709,7 +1763,7 @@ private final class NotificationServiceHandler { return true }) - content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId) + content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId, isStory: false) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index d68a5f805a..23516bfeb4 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -19,6 +19,7 @@ import ComponentDisplayAdapters import ComponentFlow import ChatFolderLinkPreviewScreen import ChatListHeaderComponent +import StoryPeerListComponent public enum ChatListContainerNodeFilter: Equatable { case all @@ -927,7 +928,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele if itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 { if case let .known(value) = offset { if value < -1.0 { - if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) { self.currentItemNode.startedScrollingAtUpperBound = true self.tempTopInset = ChatListNavigationBar.storiesScrollHeight } @@ -958,7 +959,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } let tempTopInset: CGFloat if self.currentItemNode.startedScrollingAtUpperBound { - if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) { tempTopInset = ChatListNavigationBar.storiesScrollHeight } else { tempTopInset = 0.0 @@ -1799,7 +1800,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return false } - if let storySubscriptions = controller.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = controller.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { if navigationBarComponentView.storiesUnlocked { return true @@ -2060,7 +2061,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } - if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) { self.tempAllowAvatarExpansion = true self.tempDisableStoriesAnimations = !animated self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 8b83d6cdab..03d5a266ae 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -208,6 +208,14 @@ public final class ReadBuffer: MemoryBuffer { self.offset += length } + public func readData(length: Int) -> Data { + var result = Data(count: length) + result.withUnsafeMutableBytes { buffer in + self.read(buffer.baseAddress!, offset: 0, length: length) + } + return result + } + public func skip(_ length: Int) { self.offset += length } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 8fb01d1a7b..3bfd706e49 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1318,6 +1318,10 @@ public final class Transaction { public func getStory(id: StoryId) -> CodableEntry? { return self.postbox!.getStory(id: id) } + + public func getExpiredStoryIds(belowTimestamp: Int32) -> [StoryId] { + return self.postbox!.storyItemsTable.getExpiredIds(belowTimestamp: belowTimestamp) + } } public enum PostboxResult { diff --git a/submodules/Postbox/Sources/StoryExpirationTimeItemsView.swift b/submodules/Postbox/Sources/StoryExpirationTimeItemsView.swift new file mode 100644 index 0000000000..3958de7d00 --- /dev/null +++ b/submodules/Postbox/Sources/StoryExpirationTimeItemsView.swift @@ -0,0 +1,63 @@ +import Foundation + +public struct StoryExpirationTimeEntry: Equatable { + public var id: StoryId + public var expirationTimestamp: Int32 + + init(id: StoryId, expirationTimestamp: Int32) { + self.id = id + self.expirationTimestamp = expirationTimestamp + } +} + +final class MutableStoryExpirationTimeItemsView: MutablePostboxView { + var topEntry: StoryExpirationTimeEntry? + + init(postbox: PostboxImpl) { + let _ = self.refreshDueToExternalTransaction(postbox: postbox) + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + if !transaction.storyItemsEvents.isEmpty { + var refresh = false + loop: for event in transaction.storyItemsEvents { + switch event { + case .replace: + refresh = true + break loop + } + } + if refresh { + updated = self.refreshDueToExternalTransaction(postbox: postbox) + } + } + + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + var topEntry: StoryExpirationTimeEntry? + if let item = postbox.storyItemsTable.getMinExpirationTimestamp() { + topEntry = StoryExpirationTimeEntry(id: item.0, expirationTimestamp: item.1) + } + if self.topEntry != topEntry { + self.topEntry = topEntry + return true + } else { + return false + } + } + + func immutableView() -> PostboxView { + return StoryExpirationTimeItemsView(self) + } +} + +public final class StoryExpirationTimeItemsView: PostboxView { + public let topEntry: StoryExpirationTimeEntry? + + init(_ view: MutableStoryExpirationTimeItemsView) { + self.topEntry = view.topEntry + } +} diff --git a/submodules/Postbox/Sources/StoryItemsTable.swift b/submodules/Postbox/Sources/StoryItemsTable.swift index 05d1bfabcd..098d401bfc 100644 --- a/submodules/Postbox/Sources/StoryItemsTable.swift +++ b/submodules/Postbox/Sources/StoryItemsTable.swift @@ -3,13 +3,16 @@ import Foundation public final class StoryItemsTableEntry: Equatable { public let value: CodableEntry public let id: Int32 + public let expirationTimestamp: Int32? public init( value: CodableEntry, - id: Int32 + id: Int32, + expirationTimestamp: Int32? ) { self.value = value self.id = id + self.expirationTimestamp = expirationTimestamp } public static func ==(lhs: StoryItemsTableEntry, rhs: StoryItemsTableEntry) -> Bool { @@ -22,6 +25,9 @@ public final class StoryItemsTableEntry: Equatable { if lhs.value != rhs.value { return false } + if lhs.expirationTimestamp != rhs.expirationTimestamp { + return false + } return true } } @@ -66,8 +72,30 @@ final class StoryItemsTable: Table { self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), values: { key, value in let id = key.getInt32(8) - let entry = CodableEntry(data: value.makeData()) - result.append(StoryItemsTableEntry(value: entry, id: id)) + let entry: CodableEntry + var expirationTimestamp: Int32? + + let readBuffer = ReadBuffer(data: value.makeData()) + var magic: UInt32 = 0 + readBuffer.read(&magic, offset: 0, length: 4) + if magic == 0xabcd1234 { + var length: Int32 = 0 + readBuffer.read(&length, offset: 0, length: 4) + if length > 0 && readBuffer.offset + Int(length) <= readBuffer.length { + entry = CodableEntry(data: readBuffer.readData(length: Int(length))) + if readBuffer.offset + 4 <= readBuffer.length { + var expirationTimestampValue: Int32 = 0 + readBuffer.read(&expirationTimestampValue, offset: 0, length: 4) + expirationTimestamp = expirationTimestampValue + } + } else { + entry = CodableEntry(data: Data()) + } + } else { + entry = CodableEntry(data: value.makeData()) + } + + result.append(StoryItemsTableEntry(value: entry, id: id, expirationTimestamp: expirationTimestamp)) return true }, limit: 10000) @@ -75,6 +103,80 @@ final class StoryItemsTable: Table { return result } + func getExpiredIds(belowTimestamp: Int32) -> [StoryId] { + var ids: [StoryId] = [] + + self.valueBox.scan(self.table, values: { key, value in + let peerId = PeerId(key.getInt64(0)) + let id = key.getInt32(8) + var expirationTimestamp: Int32? + + let readBuffer = ReadBuffer(data: value.makeData()) + var magic: UInt32 = 0 + readBuffer.read(&magic, offset: 0, length: 4) + if magic == 0xabcd1234 { + var length: Int32 = 0 + readBuffer.read(&length, offset: 0, length: 4) + if length > 0 && readBuffer.offset + Int(length) <= readBuffer.length { + readBuffer.skip(Int(length)) + if readBuffer.offset + 4 <= readBuffer.length { + var expirationTimestampValue: Int32 = 0 + readBuffer.read(&expirationTimestampValue, offset: 0, length: 4) + expirationTimestamp = expirationTimestampValue + } + } + } + + if let expirationTimestamp = expirationTimestamp { + if expirationTimestamp <= belowTimestamp { + ids.append(StoryId(peerId: peerId, id: id)) + } + } + + return true + }) + + return ids + } + + func getMinExpirationTimestamp() -> (StoryId, Int32)? { + var minValue: (StoryId, Int32)? + self.valueBox.scan(self.table, values: { key, value in + let peerId = PeerId(key.getInt64(0)) + let id = key.getInt32(8) + var expirationTimestamp: Int32? + + let readBuffer = ReadBuffer(data: value.makeData()) + var magic: UInt32 = 0 + readBuffer.read(&magic, offset: 0, length: 4) + if magic == 0xabcd1234 { + var length: Int32 = 0 + readBuffer.read(&length, offset: 0, length: 4) + if length > 0 && readBuffer.offset + Int(length) <= readBuffer.length { + readBuffer.skip(Int(length)) + if readBuffer.offset + 4 <= readBuffer.length { + var expirationTimestampValue: Int32 = 0 + readBuffer.read(&expirationTimestampValue, offset: 0, length: 4) + expirationTimestamp = expirationTimestampValue + } + } + } + + if let expirationTimestamp = expirationTimestamp { + if let (_, currentTimestamp) = minValue { + if expirationTimestamp < currentTimestamp { + minValue = (StoryId(peerId: peerId, id: id), expirationTimestamp) + } + } else { + minValue = (StoryId(peerId: peerId, id: id), expirationTimestamp) + } + } + + return true + }) + return minValue + } + public func replace(peerId: PeerId, entries: [StoryItemsTableEntry], events: inout [Event]) { var previousKeys: [ValueBoxKey] = [] self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), keys: { key in @@ -86,8 +188,23 @@ final class StoryItemsTable: Table { self.valueBox.remove(self.table, key: key, secure: true) } + let buffer = WriteBuffer() for entry in entries { - self.valueBox.set(self.table, key: self.key(Key(peerId: peerId, id: entry.id)), value: MemoryBuffer(data: entry.value.data)) + buffer.reset() + + var magic: UInt32 = 0xabcd1234 + buffer.write(&magic, length: 4) + + var length: Int32 = Int32(entry.value.data.count) + buffer.write(&length, length: 4) + buffer.write(entry.value.data) + + if let expirationTimestamp = entry.expirationTimestamp { + var expirationTimestampValue: Int32 = expirationTimestamp + buffer.write(&expirationTimestampValue, length: 4) + } + + self.valueBox.set(self.table, key: self.key(Key(peerId: peerId, id: entry.id)), value: buffer.readBufferNoCopy()) } events.append(.replace(peerId: peerId)) diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 4b5f8230c3..bab78e7fe1 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -43,6 +43,7 @@ public enum PostboxViewKey: Hashable { case storySubscriptions(key: PostboxStorySubscriptionsKey) case storiesState(key: PostboxStoryStatesKey) case storyItems(peerId: PeerId) + case storyExpirationTimeItems public func hash(into hasher: inout Hasher) { switch self { @@ -144,6 +145,8 @@ public enum PostboxViewKey: Hashable { hasher.combine(key) case let .storyItems(peerId): hasher.combine(peerId) + case .storyExpirationTimeItems: + hasher.combine(19) } } @@ -401,6 +404,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case .storyExpirationTimeItems: + if case .storyExpirationTimeItems = rhs { + return true + } else { + return false + } } } } @@ -491,5 +500,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableStoryStatesView(postbox: postbox, key: key) case let .storyItems(peerId): return MutableStoryItemsView(postbox: postbox, peerId: peerId) + case .storyExpirationTimeItems: + return MutableStoryExpirationTimeItemsView(postbox: postbox) } } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index f5bd08671c..18fb980468 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1155,17 +1155,24 @@ public class Account { self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: true).start()) 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(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) 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.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 : [] }, - self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] } + //self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] } ] let importantBackgroundOperations: [Signal] = extractedExpr let importantBackgroundOperationsRunning = combineLatest(queue: Queue(), importantBackgroundOperations) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a713c032f9..db16bedfef 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4513,12 +4513,12 @@ func replayFinalState( if let currentIndex = updatedPeerEntries.firstIndex(where: { $0.id == storedItem.id }) { if case .item = storedItem { if let codedEntry = CodableEntry(storedItem) { - updatedPeerEntries[currentIndex] = StoryItemsTableEntry(value: codedEntry, id: storedItem.id) + updatedPeerEntries[currentIndex] = StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp) } } } else { if let codedEntry = CodableEntry(storedItem) { - updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) + updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp)) } } } else { diff --git a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift index 2f79891113..fb1760d897 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift @@ -128,3 +128,68 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe } } } + +func managedAutoexpireStoryOperations(network: Network, postbox: Postbox) -> Signal { + return Signal { _ in + let timeOffsetOnce = Signal { subscriber in + subscriber.putNext(network.globalTimeDifference) + return EmptyDisposable + } + + let timeOffset = ( + timeOffsetOnce + |> then( + Signal.complete() + |> delay(1.0, queue: .mainQueue()) + ) + ) + |> restart + |> map { value -> Double in + round(value) + } + |> distinctUntilChanged + + Logger.shared.log("Autoexpire stories", "starting") + + let currentDisposable = MetaDisposable() + + let disposable = combineLatest(timeOffset, postbox.combinedView(keys: [PostboxViewKey.storyExpirationTimeItems])).start(next: { timeOffset, views in + guard let view = views.views[PostboxViewKey.storyExpirationTimeItems] as? StoryExpirationTimeItemsView, let topItem = view.topEntry else { + currentDisposable.set(nil) + return + } + + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset + let delay = max(0.0, Double(topItem.expirationTimestamp) - timestamp) + + let signal = Signal.complete() + |> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue()) + |> then(postbox.transaction { transaction -> Void in + var idsByPeerId: [PeerId: [Int32]] = [:] + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset) + + for id in transaction.getExpiredStoryIds(belowTimestamp: timestamp + 3) { + if idsByPeerId[id.peerId] == nil { + idsByPeerId[id.peerId] = [id.id] + } else { + idsByPeerId[id.peerId]?.append(id.id) + } + } + + for (peerId, ids) in idsByPeerId { + var items = transaction.getStoryItems(peerId: peerId) + items.removeAll(where: { ids.contains($0.id) }) + transaction.setStoryItems(peerId: topItem.id.peerId, items: items) + } + }) + + currentDisposable.set(signal.start()) + }) + + return ActionDisposable { + disposable.dispose() + currentDisposable.dispose() + } + } +} + diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3b3dc3833c..62a2baf099 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -348,6 +348,15 @@ public enum Stories { } } + public var expirationTimestamp: Int32 { + switch self { + case let .item(item): + return item.expirationTimestamp + case let .placeholder(placeholder): + return placeholder.expirationTimestamp + } + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -466,6 +475,7 @@ public final class EngineStorySubscriptions: Equatable { public let peer: EnginePeer public let hasUnseen: Bool public let hasUnseenCloseFriends: Bool + public let hasPending: Bool public let storyCount: Int public let unseenCount: Int public let lastTimestamp: Int32 @@ -474,6 +484,7 @@ public final class EngineStorySubscriptions: Equatable { peer: EnginePeer, hasUnseen: Bool, hasUnseenCloseFriends: Bool, + hasPending: Bool, storyCount: Int, unseenCount: Int, lastTimestamp: Int32 @@ -481,6 +492,7 @@ public final class EngineStorySubscriptions: Equatable { self.peer = peer self.hasUnseen = hasUnseen self.hasUnseenCloseFriends = hasUnseenCloseFriends + self.hasPending = hasPending self.storyCount = storyCount self.unseenCount = unseenCount self.lastTimestamp = lastTimestamp @@ -826,7 +838,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isCloseFriends: item.isCloseFriends ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { - items.append(StoryItemsTableEntry(value: entry, id: item.id)) + items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)) } updatedItems.append(updatedItem) } @@ -996,7 +1008,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isCloseFriends: item.isCloseFriends ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { - items[index] = StoryItemsTableEntry(value: entry, id: item.id) + items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) } updatedItems.append(updatedItem) @@ -1127,7 +1139,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isCloseFriends: item.isCloseFriends ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { - items[index] = StoryItemsTableEntry(value: entry, id: item.id) + items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) } updatedItems.append(updatedItem) @@ -1737,7 +1749,7 @@ func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) -> if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) { if case .item = updatedItem { if let entry = CodableEntry(updatedItem) { - currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id) + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index dcfad88ab4..55883f825d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -333,7 +333,7 @@ public final class StorySubscriptionsContext { updatedPeerEntries.append(previousEntry) } else { if let codedEntry = CodableEntry(storedItem) { - updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) + updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp)) } } } @@ -990,7 +990,7 @@ public final class PeerExpiringStoryListContext { updatedPeerEntries.append(previousEntry) } else { if let codedEntry = CodableEntry(storedItem) { - updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) + updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp)) } } } @@ -1148,7 +1148,7 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun updatedPeerEntries.append(previousEntry) } else { if let codedEntry = CodableEntry(storedItem) { - updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id)) + updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp)) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 33d644f41f..1544e1d43d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -645,6 +645,7 @@ public extension TelegramEngine { additionalDataKeys.append(PostboxViewKey.storyItems(peerId: self.account.peerId)) additionalDataKeys.append(PostboxViewKey.storiesState(key: .peer(self.account.peerId))) + additionalDataKeys.append(PostboxViewKey.storiesState(key: .local)) var subscriptionPeerIds = storySubscriptionsView.peerIds.filter { $0 != self.account.peerId } if !debugTimer { @@ -680,6 +681,7 @@ public extension TelegramEngine { peer: EnginePeer(accountPeer), hasUnseen: false, hasUnseenCloseFriends: false, + hasPending: false, storyCount: 0, unseenCount: 0, lastTimestamp: 0 @@ -696,14 +698,17 @@ public extension TelegramEngine { var hasUnseen = false var hasUnseenCloseFriends = false var unseenCount = 0 + var hasPending = false if let peerState = peerState { hasUnseen = peerState.maxReadId < lastEntry.id for item in itemsView.items { if item.id > peerState.maxReadId { unseenCount += 1 - - if case let .item(item) = item.value.get(Stories.StoredItem.self) { + } + + if case let .item(item) = item.value.get(Stories.StoredItem.self) { + if item.id > peerState.maxReadId { if item.isCloseFriends { hasUnseenCloseFriends = true } @@ -712,10 +717,17 @@ public extension TelegramEngine { } } + if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = view.value?.get(Stories.LocalState.self) { + if !localState.items.isEmpty { + hasPending = true + } + } + let item = EngineStorySubscriptions.Item( peer: EnginePeer(accountPeer), hasUnseen: hasUnseen, hasUnseenCloseFriends: hasUnseenCloseFriends, + hasPending: hasPending, storyCount: itemsView.items.count, unseenCount: unseenCount, lastTimestamp: lastEntry.timestamp @@ -766,6 +778,7 @@ public extension TelegramEngine { peer: EnginePeer(peer), hasUnseen: hasUnseen, hasUnseenCloseFriends: hasUnseenCloseFriends, + hasPending: false, storyCount: itemsView.items.count, unseenCount: unseenCount, lastTimestamp: lastEntry.timestamp @@ -956,7 +969,7 @@ public extension TelegramEngine { isCloseFriends: item.isCloseFriends )) if let entry = CodableEntry(updatedItem) { - currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id) + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) } } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6e202f446e..12ef5eb5a8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1458,7 +1458,7 @@ public class CameraScreen: ViewController { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "Enable Dual Camera Mode", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "Enable Dual Camera Mode", location: .point(location, .top), displayDuration: .manual(false), inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) self.controller?.present(tooltipController, in: .current) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 3b1bb6298d..cfd558d152 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -847,14 +847,6 @@ public final class ChatListHeaderComponent: Component { } let sideContentWidth: CGFloat = 0.0 - /*if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { - sideContentWidth = self.storyPeerListExternalState.collapsedWidth + 12.0 - } - if let chatListTitle = primaryContent.chatListTitle { - if chatListTitle.activity { - sideContentWidth = 0.0 - } - }*/ primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 6c2d8421f7..d715c9f2ea 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -610,44 +610,6 @@ public final class ChatListNavigationBar: Component { return size } - - /*private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) { - guard let component = self.component else { - return - } - self.applyScrollFractionAnimator?.invalidate() - self.applyScrollFractionAnimator = nil - - let storiesUnlocked = component.storiesUnlocked - - self.storiesOffsetStartFraction = self.storiesOffsetFraction - self.storiesUnlockedStartFraction = self.storiesUnlockedFraction - - self.applyScrollFraction = 0.0 - self.applyScrollUnlockedFraction = 0.0 - self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in - guard let self else { - return - } - - let t = listViewAnimationCurveSystem(value) - self.applyScrollFraction = t - if animateScrollUnlocked { - self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t) - } - - if let rawScrollOffset = self.rawScrollOffset { - self.hasDeferredScrollOffset = true - self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: .immediate) - } - }, completion: { [weak self] in - guard let self else { - return - } - self.applyScrollFractionAnimator?.invalidate() - self.applyScrollFractionAnimator = nil - }) - }*/ } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index f87016398f..ae9d2a0954 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2341,7 +2341,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual(false), inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) self.controller?.present(tooltipController, in: .current) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift index 596920b3df..90511dd49b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift @@ -240,10 +240,10 @@ private final class BannerComponent: Component { } } -final class SaveProgressScreenComponent: Component { - typealias EnvironmentType = ViewControllerComponentContainer.Environment +public final class SaveProgressScreenComponent: Component { + public typealias EnvironmentType = ViewControllerComponentContainer.Environment - enum Content: Equatable { + public enum Content: Equatable { enum ContentType: Equatable { case progress case completion @@ -262,11 +262,11 @@ final class SaveProgressScreenComponent: Component { } } - let context: AccountContext - let content: Content - let cancel: () -> Void + public let context: AccountContext + public let content: Content + public let cancel: () -> Void - init( + public init( context: AccountContext, content: Content, cancel: @escaping () -> Void @@ -276,7 +276,7 @@ final class SaveProgressScreenComponent: Component { self.cancel = cancel } - static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool { + public static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -374,7 +374,7 @@ final class SaveProgressScreenComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View() } @@ -385,7 +385,7 @@ final class SaveProgressScreenComponent: Component { private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) -final class SaveProgressScreen: ViewController { +public final class SaveProgressScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: SaveProgressScreen? private let context: AccountContext @@ -501,7 +501,7 @@ final class SaveProgressScreen: ViewController { } fileprivate let context: AccountContext - var content: SaveProgressScreenComponent.Content { + public var content: SaveProgressScreenComponent.Content { didSet { if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) @@ -514,7 +514,7 @@ final class SaveProgressScreen: ViewController { public var cancelled: () -> Void = {} - init(context: AccountContext, content: SaveProgressScreenComponent.Content) { + public init(context: AccountContext, content: SaveProgressScreenComponent.Content) { self.context = context self.content = content @@ -527,11 +527,11 @@ final class SaveProgressScreen: ViewController { self.maybeSetupDismissTimer() } - required init(coder aDecoder: NSCoder) { + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func loadDisplayNode() { + override public func loadDisplayNode() { self.displayNode = Node(controller: self) super.displayNodeDidLoad() @@ -567,7 +567,7 @@ final class SaveProgressScreen: ViewController { } private var validLayout: ContainerViewLayout? - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 7a06ea4503..663c88d61b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -67,6 +67,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerReportScreen", "//submodules/LocalMediaResources", "//submodules/SaveToCameraRoll", + "//submodules/Components/BundleIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index e49dfa2213..f51a16f98f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -5,6 +5,7 @@ import ComponentFlow import SwiftSignalKit import TelegramCore import Postbox +import TelegramPresentationData public final class StoryContentItem { public final class ExternalState { @@ -33,17 +34,20 @@ public final class StoryContentItem { public final class Environment: Equatable { public let externalState: ExternalState public let sharedState: SharedState + public let theme: PresentationTheme public let presentationProgressUpdated: (Double, Bool) -> Void public let markAsSeen: (StoryId) -> Void public init( externalState: ExternalState, sharedState: SharedState, + theme: PresentationTheme, presentationProgressUpdated: @escaping (Double, Bool) -> Void, markAsSeen: @escaping (StoryId) -> Void ) { self.externalState = externalState self.sharedState = sharedState + self.theme = theme self.presentationProgressUpdated = presentationProgressUpdated self.markAsSeen = markAsSeen } @@ -55,6 +59,9 @@ public final class StoryContentItem { if lhs.sharedState !== rhs.sharedState { return false } + if lhs.theme !== rhs.theme { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 797752ef4b..dc4b2db797 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -28,6 +28,7 @@ import TextFieldComponent import TextFormat import LocalMediaResources import SaveToCameraRoll +import BundleIconComponent public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -251,6 +252,8 @@ public final class StoryItemSetContainerComponent: Component { var centerInfoItem: InfoItem? var rightInfoItem: InfoItem? + var closeFriendIcon: ComponentView? + var captionItem: CaptionItem? let inputBackground = ComponentView() @@ -580,6 +583,9 @@ public final class StoryItemSetContainerComponent: Component { if self.sendMessageContext.shareController != nil { return true } + if self.sendMessageContext.tooltipScreen != nil { + return true + } if let navigationController = component.controller()?.navigationController as? NavigationController { let topViewController = navigationController.topViewController if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) { @@ -615,6 +621,7 @@ public final class StoryItemSetContainerComponent: Component { let itemEnvironment = StoryContentItem.Environment( externalState: visibleItem.externalState, sharedState: component.storyItemSharedState, + theme: component.theme, presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in guard let self = self, let component = self.component else { return @@ -650,7 +657,6 @@ public final class StoryItemSetContainerComponent: Component { ) if let view = visibleItem.view.view { if view.superview == nil { - view.isUserInteractionEnabled = false self.contentContainerView.insertSubview(view, at: 0) } itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size)) @@ -1133,6 +1139,15 @@ public final class StoryItemSetContainerComponent: Component { } } } + + var isUnsupported = false + var disabledPlaceholder: String? + if component.slice.peer.isService { + disabledPlaceholder = "You can't reply to this story" + } else if case .unsupported = component.slice.item.storyItem.media { + isUnsupported = true + disabledPlaceholder = "You can't reply to this story" + } let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self) @@ -1368,7 +1383,7 @@ public final class StoryItemSetContainerComponent: Component { displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular, bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, hideKeyboard: self.sendMessageContext.currentInputMode == .media, - disabledPlaceholder: component.slice.peer.isService ? "You can't reply to this story" : nil + disabledPlaceholder: disabledPlaceholder )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) @@ -1619,10 +1634,22 @@ public final class StoryItemSetContainerComponent: Component { ), nil) } }))) - items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in + + let saveText: String + if case .file = component.slice.item.storyItem.media { + saveText = "Save Video" + } else { + saveText = "Save Image" + } + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { _, a in + }, action: { [weak self] _, a in a(.default) + + guard let self else { + return + } + self.requestSave() }))) if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { @@ -1863,6 +1890,66 @@ public final class StoryItemSetContainerComponent: Component { } } + if component.slice.item.storyItem.isCloseFriends && component.slice.peer.id != component.context.account.peerId { + let closeFriendIcon: ComponentView + var closeFriendIconTransition = transition + if let current = self.closeFriendIcon { + closeFriendIcon = current + } else { + closeFriendIconTransition = .immediate + closeFriendIcon = ComponentView() + self.closeFriendIcon = closeFriendIcon + } + let closeFriendIconSize = closeFriendIcon.update( + transition: closeFriendIconTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Stories/CloseStoryIcon", + tintColor: nil, + maxSize: nil + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let closeFriendIconView = self.closeFriendIcon?.view else { + return + } + let tooltipScreen = TooltipScreen( + account: component.context.account, + sharedContext: component.context.sharedContext, + text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends.", style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in + return .dismiss(consume: false) + } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let closeFriendIconFrame = CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - 52.0 - closeFriendIconSize.width, y: 21.0), size: closeFriendIconSize) + if let closeFriendIconView = closeFriendIcon.view { + if closeFriendIconView.superview == nil { + self.contentContainerView.addSubview(closeFriendIconView) + closeFriendIconTransition.setFrame(view: closeFriendIconView, frame: closeFriendIconFrame) + } + } + } else if let closeFriendIcon = self.closeFriendIcon { + self.closeFriendIcon = nil + closeFriendIcon.view?.removeFromSuperview() + } + let gradientHeight: CGFloat = 74.0 transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight))) transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) @@ -1898,7 +1985,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if !component.slice.item.storyItem.text.isEmpty { + if !isUnsupported, !component.slice.item.storyItem.text.isEmpty { var captionItemTransition = transition let captionItem: CaptionItem if let current = self.captionItem { @@ -2686,6 +2773,35 @@ public final class StoryItemSetContainerComponent: Component { // } // }) } + + private func requestSave() { + guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { + return + } + + let saveScreen = SaveProgressScreen(context: component.context, content: .progress("Saving", 0.0)) + component.controller()?.present(saveScreen, in: .current) + + let disposable = (saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia())) + |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in + guard let saveScreen else { + return + } + saveScreen.content = .progress("Saving", progress) + }, completed: { [weak saveScreen] in + guard let saveScreen else { + return + } + saveScreen.content = .completion("Saved") + Queue.mainQueue().after(3.0, { [weak saveScreen] in + saveScreen?.dismiss() + }) + }) + + saveScreen.cancelled = { + disposable.dispose() + } + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 3be3ddd6e2..d5ebc78cb3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -48,6 +48,7 @@ final class StoryItemSetContainerSendMessage { weak var attachmentController: AttachmentController? weak var shareController: ShareController? + weak var tooltipScreen: ViewController? var currentInputMode: InputMode = .text private var needsInputActivation = false diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD index de30c2ebad..0745d5d430 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD @@ -23,6 +23,9 @@ swift_library( "//submodules/TelegramUniversalVideoContent", "//submodules/AvatarNode", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 76f0ca8379..ef728d81df 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -437,6 +437,7 @@ public final class StoryContentContextImpl: StoryContentContext { peer: peer, hasUnseen: state.hasUnseen, hasUnseenCloseFriends: state.hasUnseenCloseFriends, + hasPending: false, storyCount: state.items.count, unseenCount: 0, lastTimestamp: state.items.last?.timestamp ?? 0 diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index aff3a109c4..5c42c04874 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -12,6 +12,9 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import StoryContainerScreen import HierarchyTrackingLayer +import ButtonComponent +import MultilineTextComponent +import TelegramPresentationData final class StoryItemContentComponent: Component { typealias EnvironmentType = StoryContentItem.Environment @@ -38,56 +41,6 @@ final class StoryItemContentComponent: Component { } return true } - - /*static func preload(context: AccountContext, message: EngineMessage) -> Signal { - var messageMedia: EngineMedia? - for media in message.media { - switch media { - case let image as TelegramMediaImage: - messageMedia = .image(image) - case let file as TelegramMediaFile: - messageMedia = .file(file) - default: - break - } - } - - guard let messageMedia else { - return .complete() - } - - var fetchSignal: Signal? - switch messageMedia { - case let .image(image): - if let representation = image.representations.last { - fetchSignal = fetchedMediaResource( - mediaBox: context.account.postbox.mediaBox, - userLocation: .peer(message.id.peerId), - userContentType: .image, - reference: ImageMediaReference.message(message: MessageReference(message._asMessage()), media: image).resourceReference(representation.resource) - ) - |> ignoreValues - |> `catch` { _ -> Signal in - return .complete() - } - } - case let .file(file): - fetchSignal = fetchedMediaResource( - mediaBox: context.account.postbox.mediaBox, - userLocation: .peer(message.id.peerId), - userContentType: .image, - reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource) - ) - |> ignoreValues - |> `catch` { _ -> Signal in - return .complete() - } - default: - break - } - - return fetchSignal ?? .complete() - }*/ final class View: StoryContentItem.View { private let imageNode: TransformImageNode @@ -100,6 +53,9 @@ final class StoryItemContentComponent: Component { private weak var state: EmptyComponentState? private var environment: StoryContentItem.Environment? + private var unsupportedText: ComponentView? + private var unsupportedButton: ComponentView? + private var isProgressPaused: Bool = false private var currentProgressTimer: SwiftSignalKit.Timer? private var currentProgressTimerValue: Double = 0.0 @@ -353,10 +309,20 @@ final class StoryItemContentComponent: Component { self.environment?.presentationProgressUpdated(clippedProgress, false) } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let unsupportedButtonView = self.unsupportedButton?.view { + if let result = unsupportedButtonView.hitTest(self.convert(point, to: unsupportedButtonView), with: event) { + return result + } + } + return nil + } + func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state - self.environment = environment[StoryContentItem.Environment.self].value + let environment = environment[StoryContentItem.Environment.self].value + self.environment = environment let peerReference = PeerReference(component.peer._asPeer()) @@ -366,6 +332,8 @@ final class StoryItemContentComponent: Component { messageMedia = .image(image) case let .file(file): messageMedia = .file(file) + case .unsupported: + self.contentLoaded = true default: break } @@ -504,6 +472,99 @@ final class StoryItemContentComponent: Component { } } + switch component.item.media { + case .image, .file: + if let unsupportedText = self.unsupportedText { + self.unsupportedText = nil + unsupportedText.view?.removeFromSuperview() + } + if let unsupportedButton = self.unsupportedButton { + self.unsupportedButton = nil + unsupportedButton.view?.removeFromSuperview() + } + + self.backgroundColor = .black + default: + var unsuportedTransition = transition + + let unsupportedText: ComponentView + if let current = self.unsupportedText { + unsupportedText = current + } else { + unsuportedTransition = .immediate + unsupportedText = ComponentView() + self.unsupportedText = unsupportedText + } + + let unsupportedButton: ComponentView + if let current = self.unsupportedButton { + unsupportedButton = current + } else { + unsuportedTransition = .immediate + unsupportedButton = ComponentView() + self.unsupportedButton = unsupportedButton + } + + //TODO:localize + let unsupportedTextSize = unsupportedText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "This story is not supported by\nyour version of Telegram.", font: Font.regular(17.0), textColor: .white)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + let unsupportedButtonSize = unsupportedButton.update( + transition: unsuportedTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.7) + ), + content: AnyComponentWithIdentity(id: AnyHashable(""), component: AnyComponent(Text(text: "Update Telegram", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor + ))), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.context.sharedContext.applicationBindings.openAppStorePage() + } + )), + environment: {}, + containerSize: CGSize(width: 240.0, height: 50.0) + ) + + let spacing: CGFloat = 24.0 + let contentHeight = unsupportedTextSize.height + unsupportedButtonSize.height + spacing + var contentY = floor((availableSize.height - contentHeight) * 0.5) + + let unsupportedTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - unsupportedTextSize.width) * 0.5), y: contentY), size: unsupportedTextSize) + contentY += unsupportedTextSize.height + spacing + + let unsupportedButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - unsupportedButtonSize.width) * 0.5), y: contentY), size: unsupportedButtonSize) + + if let unsupportedTextView = unsupportedText.view { + if unsupportedTextView.superview == nil { + self.addSubview(unsupportedTextView) + } + unsuportedTransition.setPosition(view: unsupportedTextView, position: unsupportedTextFrame.center) + unsupportedTextView.bounds = CGRect(origin: CGPoint(), size: unsupportedTextFrame.size) + } + if let unsupportedButtonView = unsupportedButton.view { + if unsupportedButtonView.superview == nil { + self.addSubview(unsupportedButtonView) + } + unsuportedTransition.setFrame(view: unsupportedButtonView, frame: unsupportedButtonFrame) + } + + self.backgroundColor = UIColor(rgb: 0x181818) + } + self.updateIsProgressPaused() return availableSize diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 8087785735..1e44bfc724 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -11,6 +11,16 @@ import SwiftSignalKit import TelegramPresentationData import StoryContainerScreen +public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool { + if !storySubscriptions.items.isEmpty { + return true + } + if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) { + return true + } + return false +} + private func solveParabolicMotion(from sourcePoint: CGPoint, to targetPosition: CGPoint, progress: CGFloat) -> CGPoint { if sourcePoint.y == targetPosition.y { return sourcePoint.interpolate(to: targetPosition, amount: progress) @@ -310,7 +320,7 @@ public final class StoryPeerListComponent: Component { public func setPreviewedItem(signal: Signal) { self.previewedItemDisposable?.dispose() self.previewedItemDisposable = (signal |> map(\.?.peerId) |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] itemId in - guard let self else { + guard let self, let component = self.component else { return } self.previewedItemId = itemId @@ -318,6 +328,12 @@ public final class StoryPeerListComponent: Component { for (peerId, visibleItem) in self.visibleItems { if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { itemView.updateIsPreviewing(isPreviewing: peerId == itemId) + + if component.unlocked && peerId == itemId { + if !self.scrollView.bounds.intersects(itemView.frame.insetBy(dx: 20.0, dy: 0.0)) { + self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -40.0, dy: 0.0), animated: false) + } + } } } }) @@ -367,12 +383,23 @@ public final class StoryPeerListComponent: Component { } var hasStories: Bool = false - if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = component.storySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) { hasStories = true } let _ = hasStories - let collapseStartIndex = component.useHiddenList ? 0 : 1 + let collapseStartIndex: Int + if component.useHiddenList { + collapseStartIndex = 0 + } else if let storySubscriptions = component.storySubscriptions { + if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) { + collapseStartIndex = 0 + } else { + collapseStartIndex = 1 + } + } else { + collapseStartIndex = 1 + } let collapsedItemWidth: CGFloat = 24.0 let collapsedItemDistance: CGFloat = 14.0 diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 40b0b1fb7a..8ae8e466c0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -714,7 +714,7 @@ public final class StoryPeerListItemComponent: Component { titleString = "My story" } } else { - titleString = component.peer.compactDisplayTitle + titleString = component.peer.compactDisplayTitle.trimmingCharacters(in: .whitespacesAndNewlines) } var titleTransition = transition @@ -751,7 +751,7 @@ public final class StoryPeerListItemComponent: Component { maximumNumberOfLines: 1 )), environment: {}, - containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0) + containerSize: CGSize(width: availableSize.width + 12.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleSize) if let titleView = self.title.view { diff --git a/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/Contents.json new file mode 100644 index 0000000000..d8a96707c9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StoryCloseIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/StoryCloseIcon.svg b/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/StoryCloseIcon.svg new file mode 100644 index 0000000000..4a798ae8cb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/StoryCloseIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index d334e398c7..4fdbf59faf 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2663,8 +2663,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.requestAvatarExpansion?(true, self.avatarListNode.listContainerNode.galleryEntries, entry, self.avatarTransitionArguments(entry: currentEntry)) } } else if let entry = self.avatarListNode.listContainerNode.galleryEntries.first { - let _ = self.avatarListNode.avatarContainerNode.avatarNode self.requestAvatarExpansion?(false, self.avatarListNode.listContainerNode.galleryEntries, nil, self.avatarTransitionArguments(entry: entry)) + } else if let storyParams = self.avatarListNode.listContainerNode.storyParams, storyParams.count != 0 { + self.requestAvatarExpansion?(false, self.avatarListNode.listContainerNode.galleryEntries, nil, nil) } else { self.cancelUpload?() } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f14f81d909..a063708077 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3882,7 +3882,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.avatarListNode.avatarContainerNode.storyData = nil self.headerNode.avatarListNode.listContainerNode.storyParams = nil } else { - self.headerNode.avatarListNode.avatarContainerNode.storyData = (state.hasUnseen, state.hasUnseenCloseFriends) + self.headerNode.avatarListNode.avatarContainerNode.storyData = (state.hasUnseen, state.hasUnseenCloseFriends && peer.id != self.context.account.peerId) self.headerNode.avatarListNode.listContainerNode.storyParams = (peer, state.items.prefix(3).compactMap { item -> EngineStoryItem? in switch item { case let .item(item): @@ -4132,7 +4132,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view return StoryContainerScreen.TransitionOut( destinationView: transitionView, - transitionView: nil, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak transitionView] in + let parentView = UIView() + if let copyView = transitionView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), destinationRect: transitionView.bounds, destinationCornerRadius: transitionView.bounds.height * 0.5, destinationIsAvatar: true, diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 49b5f680c6..caedc905a1 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -583,7 +583,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if self.containerNode.frame.contains(point) { self.requestDismiss() return self.view - } else { + } else if case .manual(false) = self.displayDuration { return nil } } @@ -710,7 +710,7 @@ public final class TooltipScreen: ViewController { case `default` case custom(Double) case infinite - case manual + case manual(Bool) } public enum Style {