This commit is contained in:
Ali 2023-06-24 21:50:38 +03:00
parent 80587fe553
commit b76ea6ba99
17 changed files with 400 additions and 44 deletions

View File

@ -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"

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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())

View File

@ -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 {

View File

@ -128,3 +128,68 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe
}
}
}
func managedAutoexpireStoryOperations(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
return Signal { _ in
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.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<Void, NoError>.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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}

View File

@ -310,7 +310,7 @@ public final class StoryPeerListComponent: Component {
public func setPreviewedItem(signal: Signal<StoryId?, NoError>) {
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)
}
}
}
}
})

View File

@ -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 {

View File

@ -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?()
}

View File

@ -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,