Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-06-24 23:12:39 +02:00
commit 2f9eccddc6
33 changed files with 749 additions and 174 deletions

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

@ -19,6 +19,7 @@ import ComponentDisplayAdapters
import ComponentFlow
import ChatFolderLinkPreviewScreen
import ChatListHeaderComponent
import StoryPeerListComponent
public enum ChatListContainerNodeFilter: Equatable {
case all
@ -927,7 +928,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 {
if case let .known(value) = offset {
if value < -1.0 {
if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty {
if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) {
self.currentItemNode.startedScrollingAtUpperBound = true
self.tempTopInset = ChatListNavigationBar.storiesScrollHeight
}
@ -958,7 +959,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
}
let tempTopInset: CGFloat
if self.currentItemNode.startedScrollingAtUpperBound {
if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty {
if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) {
tempTopInset = ChatListNavigationBar.storiesScrollHeight
} else {
tempTopInset = 0.0
@ -1799,7 +1800,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return false
}
if let storySubscriptions = controller.orderedStorySubscriptions, !storySubscriptions.items.isEmpty {
if let storySubscriptions = controller.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarComponentView.storiesUnlocked {
return true
@ -2060,7 +2061,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return
}
if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty {
if let storySubscriptions = self.controller?.orderedStorySubscriptions, (shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) || true) {
self.tempAllowAvatarExpansion = true
self.tempDisableStoriesAnimations = !animated
self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate

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,17 +1155,24 @@ public class Account {
self.managedOperationsDisposable.add(managedCloudChatRemoveMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: true).start())
self.managedOperationsDisposable.add(managedAutoremoveMessageOperations(network: self.network, postbox: self.postbox, isRemove: false).start())
self.managedOperationsDisposable.add(managedAutoexpireStoryOperations(network: self.network, postbox: self.postbox).start())
self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start())
self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
let extractedExpr: [Signal<AccountRunningImportantTasks, NoError>] = [
managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] },
self.pendingMessageManager.hasPendingMessages |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
(self.pendingStoryManager?.hasPending ?? .single(false)) |> map { hasPending in hasPending ? AccountRunningImportantTasks.pendingMessages : [] },
self.pendingUpdateMessageManager.updatingMessageMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
(self.pendingStoryManager?.hasPending ?? .single(false)) |> map {
hasPending in hasPending ? AccountRunningImportantTasks.pendingMessages : []
},
self.pendingUpdateMessageManager.updatingMessageMedia |> map {
!$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : []
},
self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map {
!$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : []
},
self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] },
self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] }
//self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] }
]
let importantBackgroundOperations: [Signal<AccountRunningImportantTasks, NoError>] = extractedExpr
let importantBackgroundOperationsRunning = combineLatest(queue: Queue(), importantBackgroundOperations)

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)
@ -466,6 +475,7 @@ public final class EngineStorySubscriptions: Equatable {
public let peer: EnginePeer
public let hasUnseen: Bool
public let hasUnseenCloseFriends: Bool
public let hasPending: Bool
public let storyCount: Int
public let unseenCount: Int
public let lastTimestamp: Int32
@ -474,6 +484,7 @@ public final class EngineStorySubscriptions: Equatable {
peer: EnginePeer,
hasUnseen: Bool,
hasUnseenCloseFriends: Bool,
hasPending: Bool,
storyCount: Int,
unseenCount: Int,
lastTimestamp: Int32
@ -481,6 +492,7 @@ public final class EngineStorySubscriptions: Equatable {
self.peer = peer
self.hasUnseen = hasUnseen
self.hasUnseenCloseFriends = hasUnseenCloseFriends
self.hasPending = hasPending
self.storyCount = storyCount
self.unseenCount = unseenCount
self.lastTimestamp = lastTimestamp
@ -826,7 +838,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isCloseFriends: item.isCloseFriends
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id))
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp))
}
updatedItems.append(updatedItem)
}
@ -996,7 +1008,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isCloseFriends: item.isCloseFriends
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
}
updatedItems.append(updatedItem)
@ -1127,7 +1139,7 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isCloseFriends: item.isCloseFriends
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
}
updatedItems.append(updatedItem)
@ -1737,7 +1749,7 @@ func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) ->
if let updatedItem = result.first(where: { $0.id == currentItems[i].id }) {
if case .item = updatedItem {
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id)
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp)
}
}
}

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

@ -645,6 +645,7 @@ public extension TelegramEngine {
additionalDataKeys.append(PostboxViewKey.storyItems(peerId: self.account.peerId))
additionalDataKeys.append(PostboxViewKey.storiesState(key: .peer(self.account.peerId)))
additionalDataKeys.append(PostboxViewKey.storiesState(key: .local))
var subscriptionPeerIds = storySubscriptionsView.peerIds.filter { $0 != self.account.peerId }
if !debugTimer {
@ -680,6 +681,7 @@ public extension TelegramEngine {
peer: EnginePeer(accountPeer),
hasUnseen: false,
hasUnseenCloseFriends: false,
hasPending: false,
storyCount: 0,
unseenCount: 0,
lastTimestamp: 0
@ -696,14 +698,17 @@ public extension TelegramEngine {
var hasUnseen = false
var hasUnseenCloseFriends = false
var unseenCount = 0
var hasPending = false
if let peerState = peerState {
hasUnseen = peerState.maxReadId < lastEntry.id
for item in itemsView.items {
if item.id > peerState.maxReadId {
unseenCount += 1
if case let .item(item) = item.value.get(Stories.StoredItem.self) {
}
if case let .item(item) = item.value.get(Stories.StoredItem.self) {
if item.id > peerState.maxReadId {
if item.isCloseFriends {
hasUnseenCloseFriends = true
}
@ -712,10 +717,17 @@ public extension TelegramEngine {
}
}
if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = view.value?.get(Stories.LocalState.self) {
if !localState.items.isEmpty {
hasPending = true
}
}
let item = EngineStorySubscriptions.Item(
peer: EnginePeer(accountPeer),
hasUnseen: hasUnseen,
hasUnseenCloseFriends: hasUnseenCloseFriends,
hasPending: hasPending,
storyCount: itemsView.items.count,
unseenCount: unseenCount,
lastTimestamp: lastEntry.timestamp
@ -766,6 +778,7 @@ public extension TelegramEngine {
peer: EnginePeer(peer),
hasUnseen: hasUnseen,
hasUnseenCloseFriends: hasUnseenCloseFriends,
hasPending: false,
storyCount: itemsView.items.count,
unseenCount: unseenCount,
lastTimestamp: lastEntry.timestamp
@ -956,7 +969,7 @@ public extension TelegramEngine {
isCloseFriends: item.isCloseFriends
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id)
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp)
}
}
}

View File

@ -1458,7 +1458,7 @@ public class CameraScreen: ViewController {
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "Enable Dual Camera Mode", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "Enable Dual Camera Mode", location: .point(location, .top), displayDuration: .manual(false), inset: 16.0, shouldDismissOnTouch: { _ in
return .ignore
})
self.controller?.present(tooltipController, in: .current)

View File

@ -847,14 +847,6 @@ public final class ChatListHeaderComponent: Component {
}
let sideContentWidth: CGFloat = 0.0
/*if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
sideContentWidth = self.storyPeerListExternalState.collapsedWidth + 12.0
}
if let chatListTitle = primaryContent.chatListTitle {
if chatListTitle.activity {
sideContentWidth = 0.0
}
}*/
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))

View File

@ -610,44 +610,6 @@ public final class ChatListNavigationBar: Component {
return size
}
/*private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) {
guard let component = self.component else {
return
}
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
let storiesUnlocked = component.storiesUnlocked
self.storiesOffsetStartFraction = self.storiesOffsetFraction
self.storiesUnlockedStartFraction = self.storiesUnlockedFraction
self.applyScrollFraction = 0.0
self.applyScrollUnlockedFraction = 0.0
self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in
guard let self else {
return
}
let t = listViewAnimationCurveSystem(value)
self.applyScrollFraction = t
if animateScrollUnlocked {
self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t)
}
if let rawScrollOffset = self.rawScrollOffset {
self.hasDeferredScrollOffset = true
self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: .immediate)
}
}, completion: { [weak self] in
guard let self else {
return
}
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
})
}*/
}
public func makeView() -> View {

View File

@ -2341,7 +2341,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual(false), inset: 16.0, shouldDismissOnTouch: { _ in
return .ignore
})
self.controller?.present(tooltipController, in: .current)

View File

@ -240,10 +240,10 @@ private final class BannerComponent: Component {
}
}
final class SaveProgressScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
public final class SaveProgressScreenComponent: Component {
public typealias EnvironmentType = ViewControllerComponentContainer.Environment
enum Content: Equatable {
public enum Content: Equatable {
enum ContentType: Equatable {
case progress
case completion
@ -262,11 +262,11 @@ final class SaveProgressScreenComponent: Component {
}
}
let context: AccountContext
let content: Content
let cancel: () -> Void
public let context: AccountContext
public let content: Content
public let cancel: () -> Void
init(
public init(
context: AccountContext,
content: Content,
cancel: @escaping () -> Void
@ -276,7 +276,7 @@ final class SaveProgressScreenComponent: Component {
self.cancel = cancel
}
static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool {
public static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
@ -374,7 +374,7 @@ final class SaveProgressScreenComponent: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View()
}
@ -385,7 +385,7 @@ final class SaveProgressScreenComponent: Component {
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
final class SaveProgressScreen: ViewController {
public final class SaveProgressScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: SaveProgressScreen?
private let context: AccountContext
@ -501,7 +501,7 @@ final class SaveProgressScreen: ViewController {
}
fileprivate let context: AccountContext
var content: SaveProgressScreenComponent.Content {
public var content: SaveProgressScreenComponent.Content {
didSet {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
@ -514,7 +514,7 @@ final class SaveProgressScreen: ViewController {
public var cancelled: () -> Void = {}
init(context: AccountContext, content: SaveProgressScreenComponent.Content) {
public init(context: AccountContext, content: SaveProgressScreenComponent.Content) {
self.context = context
self.content = content
@ -527,11 +527,11 @@ final class SaveProgressScreen: ViewController {
self.maybeSetupDismissTimer()
}
required init(coder aDecoder: NSCoder) {
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
@ -567,7 +567,7 @@ final class SaveProgressScreen: ViewController {
}
private var validLayout: ContainerViewLayout?
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
super.containerLayoutUpdated(layout, transition: transition)

View File

@ -67,6 +67,7 @@ swift_library(
"//submodules/TelegramUI/Components/PeerReportScreen",
"//submodules/LocalMediaResources",
"//submodules/SaveToCameraRoll",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -5,6 +5,7 @@ import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
public final class StoryContentItem {
public final class ExternalState {
@ -33,17 +34,20 @@ public final class StoryContentItem {
public final class Environment: Equatable {
public let externalState: ExternalState
public let sharedState: SharedState
public let theme: PresentationTheme
public let presentationProgressUpdated: (Double, Bool) -> Void
public let markAsSeen: (StoryId) -> Void
public init(
externalState: ExternalState,
sharedState: SharedState,
theme: PresentationTheme,
presentationProgressUpdated: @escaping (Double, Bool) -> Void,
markAsSeen: @escaping (StoryId) -> Void
) {
self.externalState = externalState
self.sharedState = sharedState
self.theme = theme
self.presentationProgressUpdated = presentationProgressUpdated
self.markAsSeen = markAsSeen
}
@ -55,6 +59,9 @@ public final class StoryContentItem {
if lhs.sharedState !== rhs.sharedState {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
}

View File

@ -28,6 +28,7 @@ import TextFieldComponent
import TextFormat
import LocalMediaResources
import SaveToCameraRoll
import BundleIconComponent
public final class StoryItemSetContainerComponent: Component {
public final class ExternalState {
@ -251,6 +252,8 @@ public final class StoryItemSetContainerComponent: Component {
var centerInfoItem: InfoItem?
var rightInfoItem: InfoItem?
var closeFriendIcon: ComponentView<Empty>?
var captionItem: CaptionItem?
let inputBackground = ComponentView<Empty>()
@ -580,6 +583,9 @@ public final class StoryItemSetContainerComponent: Component {
if self.sendMessageContext.shareController != nil {
return true
}
if self.sendMessageContext.tooltipScreen != nil {
return true
}
if let navigationController = component.controller()?.navigationController as? NavigationController {
let topViewController = navigationController.topViewController
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) {
@ -615,6 +621,7 @@ public final class StoryItemSetContainerComponent: Component {
let itemEnvironment = StoryContentItem.Environment(
externalState: visibleItem.externalState,
sharedState: component.storyItemSharedState,
theme: component.theme,
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
guard let self = self, let component = self.component else {
return
@ -650,7 +657,6 @@ public final class StoryItemSetContainerComponent: Component {
)
if let view = visibleItem.view.view {
if view.superview == nil {
view.isUserInteractionEnabled = false
self.contentContainerView.insertSubview(view, at: 0)
}
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
@ -1133,6 +1139,15 @@ public final class StoryItemSetContainerComponent: Component {
}
}
}
var isUnsupported = false
var disabledPlaceholder: String?
if component.slice.peer.isService {
disabledPlaceholder = "You can't reply to this story"
} else if case .unsupported = component.slice.item.storyItem.media {
isUnsupported = true
disabledPlaceholder = "You can't reply to this story"
}
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self)
@ -1368,7 +1383,7 @@ public final class StoryItemSetContainerComponent: Component {
displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular,
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
disabledPlaceholder: component.slice.peer.isService ? "You can't reply to this story" : nil
disabledPlaceholder: disabledPlaceholder
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
@ -1619,10 +1634,22 @@ public final class StoryItemSetContainerComponent: Component {
), nil)
}
})))
items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in
let saveText: String
if case .file = component.slice.item.storyItem.media {
saveText = "Save Video"
} else {
saveText = "Save Image"
}
items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.requestSave()
})))
if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) {
@ -1863,6 +1890,66 @@ public final class StoryItemSetContainerComponent: Component {
}
}
if component.slice.item.storyItem.isCloseFriends && component.slice.peer.id != component.context.account.peerId {
let closeFriendIcon: ComponentView<Empty>
var closeFriendIconTransition = transition
if let current = self.closeFriendIcon {
closeFriendIcon = current
} else {
closeFriendIconTransition = .immediate
closeFriendIcon = ComponentView()
self.closeFriendIcon = closeFriendIcon
}
let closeFriendIconSize = closeFriendIcon.update(
transition: closeFriendIconTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Stories/CloseStoryIcon",
tintColor: nil,
maxSize: nil
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let closeFriendIconView = self.closeFriendIcon?.view else {
return
}
let tooltipScreen = TooltipScreen(
account: component.context.account,
sharedContext: component.context.sharedContext,
text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends.", style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in
return .dismiss(consume: false)
}
)
tooltipScreen.willBecomeDismissed = { [weak self] _ in
guard let self else {
return
}
self.sendMessageContext.tooltipScreen = nil
self.updateIsProgressPaused()
}
self.sendMessageContext.tooltipScreen = tooltipScreen
self.updateIsProgressPaused()
component.controller()?.present(tooltipScreen, in: .current)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let closeFriendIconFrame = CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - 52.0 - closeFriendIconSize.width, y: 21.0), size: closeFriendIconSize)
if let closeFriendIconView = closeFriendIcon.view {
if closeFriendIconView.superview == nil {
self.contentContainerView.addSubview(closeFriendIconView)
closeFriendIconTransition.setFrame(view: closeFriendIconView, frame: closeFriendIconFrame)
}
}
} else if let closeFriendIcon = self.closeFriendIcon {
self.closeFriendIcon = nil
closeFriendIcon.view?.removeFromSuperview()
}
let gradientHeight: CGFloat = 74.0
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0)
@ -1898,7 +1985,7 @@ public final class StoryItemSetContainerComponent: Component {
}
}
if !component.slice.item.storyItem.text.isEmpty {
if !isUnsupported, !component.slice.item.storyItem.text.isEmpty {
var captionItemTransition = transition
let captionItem: CaptionItem
if let current = self.captionItem {
@ -2686,6 +2773,35 @@ public final class StoryItemSetContainerComponent: Component {
// }
// })
}
private func requestSave() {
guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else {
return
}
let saveScreen = SaveProgressScreen(context: component.context, content: .progress("Saving", 0.0))
component.controller()?.present(saveScreen, in: .current)
let disposable = (saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia()))
|> deliverOnMainQueue).start(next: { [weak saveScreen] progress in
guard let saveScreen else {
return
}
saveScreen.content = .progress("Saving", progress)
}, completed: { [weak saveScreen] in
guard let saveScreen else {
return
}
saveScreen.content = .completion("Saved")
Queue.mainQueue().after(3.0, { [weak saveScreen] in
saveScreen?.dismiss()
})
})
saveScreen.cancelled = {
disposable.dispose()
}
}
}
public func makeView() -> View {

View File

@ -48,6 +48,7 @@ final class StoryItemSetContainerSendMessage {
weak var attachmentController: AttachmentController?
weak var shareController: ShareController?
weak var tooltipScreen: ViewController?
var currentInputMode: InputMode = .text
private var needsInputActivation = false

View File

@ -23,6 +23,9 @@ swift_library(
"//submodules/TelegramUniversalVideoContent",
"//submodules/AvatarNode",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",

View File

@ -437,6 +437,7 @@ public final class StoryContentContextImpl: StoryContentContext {
peer: peer,
hasUnseen: state.hasUnseen,
hasUnseenCloseFriends: state.hasUnseenCloseFriends,
hasPending: false,
storyCount: state.items.count,
unseenCount: 0,
lastTimestamp: state.items.last?.timestamp ?? 0

View File

@ -12,6 +12,9 @@ import UniversalMediaPlayer
import TelegramUniversalVideoContent
import StoryContainerScreen
import HierarchyTrackingLayer
import ButtonComponent
import MultilineTextComponent
import TelegramPresentationData
final class StoryItemContentComponent: Component {
typealias EnvironmentType = StoryContentItem.Environment
@ -38,56 +41,6 @@ final class StoryItemContentComponent: Component {
}
return true
}
/*static func preload(context: AccountContext, message: EngineMessage) -> Signal<Never, NoError> {
var messageMedia: EngineMedia?
for media in message.media {
switch media {
case let image as TelegramMediaImage:
messageMedia = .image(image)
case let file as TelegramMediaFile:
messageMedia = .file(file)
default:
break
}
}
guard let messageMedia else {
return .complete()
}
var fetchSignal: Signal<Never, NoError>?
switch messageMedia {
case let .image(image):
if let representation = image.representations.last {
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .peer(message.id.peerId),
userContentType: .image,
reference: ImageMediaReference.message(message: MessageReference(message._asMessage()), media: image).resourceReference(representation.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
case let .file(file):
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .peer(message.id.peerId),
userContentType: .image,
reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
default:
break
}
return fetchSignal ?? .complete()
}*/
final class View: StoryContentItem.View {
private let imageNode: TransformImageNode
@ -100,6 +53,9 @@ final class StoryItemContentComponent: Component {
private weak var state: EmptyComponentState?
private var environment: StoryContentItem.Environment?
private var unsupportedText: ComponentView<Empty>?
private var unsupportedButton: ComponentView<Empty>?
private var isProgressPaused: Bool = false
private var currentProgressTimer: SwiftSignalKit.Timer?
private var currentProgressTimerValue: Double = 0.0
@ -353,10 +309,20 @@ final class StoryItemContentComponent: Component {
self.environment?.presentationProgressUpdated(clippedProgress, false)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let unsupportedButtonView = self.unsupportedButton?.view {
if let result = unsupportedButtonView.hitTest(self.convert(point, to: unsupportedButtonView), with: event) {
return result
}
}
return nil
}
func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.environment = environment[StoryContentItem.Environment.self].value
let environment = environment[StoryContentItem.Environment.self].value
self.environment = environment
let peerReference = PeerReference(component.peer._asPeer())
@ -366,6 +332,8 @@ final class StoryItemContentComponent: Component {
messageMedia = .image(image)
case let .file(file):
messageMedia = .file(file)
case .unsupported:
self.contentLoaded = true
default:
break
}
@ -504,6 +472,99 @@ final class StoryItemContentComponent: Component {
}
}
switch component.item.media {
case .image, .file:
if let unsupportedText = self.unsupportedText {
self.unsupportedText = nil
unsupportedText.view?.removeFromSuperview()
}
if let unsupportedButton = self.unsupportedButton {
self.unsupportedButton = nil
unsupportedButton.view?.removeFromSuperview()
}
self.backgroundColor = .black
default:
var unsuportedTransition = transition
let unsupportedText: ComponentView<Empty>
if let current = self.unsupportedText {
unsupportedText = current
} else {
unsuportedTransition = .immediate
unsupportedText = ComponentView()
self.unsupportedText = unsupportedText
}
let unsupportedButton: ComponentView<Empty>
if let current = self.unsupportedButton {
unsupportedButton = current
} else {
unsuportedTransition = .immediate
unsupportedButton = ComponentView()
self.unsupportedButton = unsupportedButton
}
//TODO:localize
let unsupportedTextSize = unsupportedText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "This story is not supported by\nyour version of Telegram.", font: Font.regular(17.0), textColor: .white)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height)
)
let unsupportedButtonSize = unsupportedButton.update(
transition: unsuportedTransition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.7)
),
content: AnyComponentWithIdentity(id: AnyHashable(""), component: AnyComponent(Text(text: "Update Telegram", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor
))),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.applicationBindings.openAppStorePage()
}
)),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
)
let spacing: CGFloat = 24.0
let contentHeight = unsupportedTextSize.height + unsupportedButtonSize.height + spacing
var contentY = floor((availableSize.height - contentHeight) * 0.5)
let unsupportedTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - unsupportedTextSize.width) * 0.5), y: contentY), size: unsupportedTextSize)
contentY += unsupportedTextSize.height + spacing
let unsupportedButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - unsupportedButtonSize.width) * 0.5), y: contentY), size: unsupportedButtonSize)
if let unsupportedTextView = unsupportedText.view {
if unsupportedTextView.superview == nil {
self.addSubview(unsupportedTextView)
}
unsuportedTransition.setPosition(view: unsupportedTextView, position: unsupportedTextFrame.center)
unsupportedTextView.bounds = CGRect(origin: CGPoint(), size: unsupportedTextFrame.size)
}
if let unsupportedButtonView = unsupportedButton.view {
if unsupportedButtonView.superview == nil {
self.addSubview(unsupportedButtonView)
}
unsuportedTransition.setFrame(view: unsupportedButtonView, frame: unsupportedButtonFrame)
}
self.backgroundColor = UIColor(rgb: 0x181818)
}
self.updateIsProgressPaused()
return availableSize

View File

@ -11,6 +11,16 @@ import SwiftSignalKit
import TelegramPresentationData
import StoryContainerScreen
public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool {
if !storySubscriptions.items.isEmpty {
return true
}
if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) {
return true
}
return false
}
private func solveParabolicMotion(from sourcePoint: CGPoint, to targetPosition: CGPoint, progress: CGFloat) -> CGPoint {
if sourcePoint.y == targetPosition.y {
return sourcePoint.interpolate(to: targetPosition, amount: progress)
@ -310,7 +320,7 @@ public final class StoryPeerListComponent: Component {
public func setPreviewedItem(signal: Signal<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 +328,12 @@ public final class StoryPeerListComponent: Component {
for (peerId, visibleItem) in self.visibleItems {
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
itemView.updateIsPreviewing(isPreviewing: peerId == itemId)
if component.unlocked && peerId == itemId {
if !self.scrollView.bounds.intersects(itemView.frame.insetBy(dx: 20.0, dy: 0.0)) {
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -40.0, dy: 0.0), animated: false)
}
}
}
}
})
@ -367,12 +383,23 @@ public final class StoryPeerListComponent: Component {
}
var hasStories: Bool = false
if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
if let storySubscriptions = component.storySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) {
hasStories = true
}
let _ = hasStories
let collapseStartIndex = component.useHiddenList ? 0 : 1
let collapseStartIndex: Int
if component.useHiddenList {
collapseStartIndex = 0
} else if let storySubscriptions = component.storySubscriptions {
if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) {
collapseStartIndex = 0
} else {
collapseStartIndex = 1
}
} else {
collapseStartIndex = 1
}
let collapsedItemWidth: CGFloat = 24.0
let collapsedItemDistance: CGFloat = 14.0

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

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "StoryCloseIcon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,10 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="22" height="22" rx="11" fill="url(#paint0_linear_0_3)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8351 15.3336C11.1171 15.1638 11.4698 15.1638 11.7519 15.3336L14.5953 17.045C15.2688 17.4504 16.0983 16.8463 15.9193 16.081L15.1664 12.8627C15.0911 12.5408 15.2007 12.2037 15.4508 11.9876L17.9565 9.82292C18.5521 9.30844 18.2346 8.3309 17.4504 8.26456L14.1484 7.9852C13.8202 7.95742 13.5342 7.75036 13.4053 7.44716L12.1115 4.40309C11.8049 3.68149 10.7821 3.68149 10.4754 4.40309L9.18166 7.44716C9.05279 7.75036 8.7668 7.95742 8.43852 7.9852L5.13659 8.26456C4.3524 8.3309 4.0349 9.30844 4.63043 9.82292L7.13613 11.9876C7.3863 12.2037 7.49586 12.5408 7.42056 12.8627L6.6677 16.081C6.48866 16.8463 7.31817 17.4504 7.99161 17.045L10.8351 15.3336Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_0_3" x1="18.3333" y1="2.93333" x2="3.3" y2="18.7" gradientUnits="userSpaceOnUse">
<stop stop-color="#7CD636"/>
<stop offset="1" stop-color="#26B470"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

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,

View File

@ -583,7 +583,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
if self.containerNode.frame.contains(point) {
self.requestDismiss()
return self.view
} else {
} else if case .manual(false) = self.displayDuration {
return nil
}
}
@ -710,7 +710,7 @@ public final class TooltipScreen: ViewController {
case `default`
case custom(Double)
case infinite
case manual
case manual(Bool)
}
public enum Style {