mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Stories
This commit is contained in:
parent
80587fe553
commit
b76ea6ba99
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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?()
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user