mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
2f9eccddc6
@ -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()
|
||||
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()
|
||||
|
||||
source.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
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
|
||||
@ -374,16 +406,37 @@ private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [Stri
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,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)
|
||||
|
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -67,6 +67,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerReportScreen",
|
||||
"//submodules/LocalMediaResources",
|
||||
"//submodules/SaveToCameraRoll",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
@ -1134,6 +1140,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)
|
||||
self.inputPanel.parentState = state
|
||||
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -39,56 +42,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
|
||||
private var videoNode: UniversalVideoNode?
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "StoryCloseIcon.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
10
submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/StoryCloseIcon.svg
vendored
Normal file
10
submodules/TelegramUI/Images.xcassets/Stories/CloseStoryIcon.imageset/StoryCloseIcon.svg
vendored
Normal 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 |
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user