diff --git a/Telegram/BUILD b/Telegram/BUILD index 00a75be7dd..4aac94e40b 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -2013,9 +2013,9 @@ xcodeproj( "Debug": { "//command_line_option:compilation_mode": "dbg", }, - "Release": { - "//command_line_option:compilation_mode": "opt", - }, + #"Release": { + # "//command_line_option:compilation_mode": "opt", + #}, }, default_xcode_configuration = "Debug" 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/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..0590e54584 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1155,6 +1155,7 @@ 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()) 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 f8d09fc2e1..b5bc8e4807 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) @@ -826,7 +835,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 +1005,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 +1136,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 +1746,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..6d7abb1f8d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -956,7 +956,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/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 8087785735..1026eb66eb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -310,7 +310,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 +318,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) + } + } } } }) 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/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,