mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' into beta
# Conflicts: # Telegram/Telegram-iOS/en.lproj/Localizable.strings # submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift # submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift # submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift # submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift # submodules/TelegramUI/Sources/FetchVideoMediaResource.swift # submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift # versions.json
This commit is contained in:
commit
4a0399a455
@ -162,6 +162,9 @@ verify_beta_testflight:
|
|||||||
submit_appstore:
|
submit_appstore:
|
||||||
tags:
|
tags:
|
||||||
- deploy
|
- deploy
|
||||||
|
only:
|
||||||
|
- beta
|
||||||
|
- hotfix
|
||||||
stage: submit
|
stage: submit
|
||||||
needs: []
|
needs: []
|
||||||
when: manual
|
when: manual
|
||||||
|
@ -2013,9 +2013,9 @@ xcodeproj(
|
|||||||
"Debug": {
|
"Debug": {
|
||||||
"//command_line_option:compilation_mode": "dbg",
|
"//command_line_option:compilation_mode": "dbg",
|
||||||
},
|
},
|
||||||
"Release": {
|
#"Release": {
|
||||||
"//command_line_option:compilation_mode": "opt",
|
# "//command_line_option:compilation_mode": "opt",
|
||||||
},
|
#},
|
||||||
},
|
},
|
||||||
default_xcode_configuration = "Debug"
|
default_xcode_configuration = "Debug"
|
||||||
|
|
||||||
|
@ -301,15 +301,43 @@ private func testAvatarImage(size: CGSize) -> UIImage? {
|
|||||||
return image
|
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)
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||||
let context = UIGraphicsGetCurrentContext()
|
let context = UIGraphicsGetCurrentContext()
|
||||||
|
|
||||||
context?.beginPath()
|
if isStory {
|
||||||
context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
let lineWidth: CGFloat = 2.0
|
||||||
context?.clip()
|
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))
|
||||||
source.draw(in: CGRect(origin: CGPoint(), size: size))
|
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()
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
@ -332,12 +360,16 @@ private let gradientColors: [NSArray] = [
|
|||||||
[UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor],
|
[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)
|
UIGraphicsBeginImageContextWithOptions(size, false, 2.0)
|
||||||
let context = UIGraphicsGetCurrentContext()
|
let context = UIGraphicsGetCurrentContext()
|
||||||
|
|
||||||
context?.beginPath()
|
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()
|
context?.clip()
|
||||||
|
|
||||||
let colorIndex: Int
|
let colorIndex: Int
|
||||||
@ -373,17 +405,38 @@ private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [Stri
|
|||||||
CTLineDraw(line, context)
|
CTLineDraw(line, context)
|
||||||
}
|
}
|
||||||
context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
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()
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
private func avatarImage(path: String?, peerId: PeerId, letters: [String], size: CGSize) -> UIImage {
|
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) {
|
if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image, isStory: isStory) {
|
||||||
return roundImage
|
return roundImage
|
||||||
} else {
|
} 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, *)
|
@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) {
|
if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) {
|
||||||
let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents.png", keepDuration: .shortLived)
|
let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents\(isStory ? "-story2" : "").png", keepDuration: .shortLived)
|
||||||
if let _ = fileSize(cachedPath) {
|
if let _ = fileSize(cachedPath), !"".isEmpty {
|
||||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||||
} else {
|
} 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() {
|
if let data = image.pngData() {
|
||||||
|
let _ = try? FileManager.default.removeItem(atPath: cachedPath)
|
||||||
let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
|
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) {
|
if let _ = fileSize(cachedPath) {
|
||||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||||
} else {
|
} 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() {
|
if let data = image.pngData() {
|
||||||
let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
|
let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
|
||||||
}
|
}
|
||||||
@ -468,9 +522,9 @@ private struct NotificationContent: CustomStringConvertible {
|
|||||||
return string
|
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, *) {
|
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
|
self.senderImage = image
|
||||||
|
|
||||||
@ -847,6 +901,7 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
var peerId: PeerId?
|
var peerId: PeerId?
|
||||||
var messageId: MessageId.Id?
|
var messageId: MessageId.Id?
|
||||||
|
var storyId: Int32?
|
||||||
var mediaAttachment: Media?
|
var mediaAttachment: Media?
|
||||||
var downloadNotificationSound: (file: TelegramMediaFile, path: String, fileName: String)?
|
var downloadNotificationSound: (file: TelegramMediaFile, path: String, fileName: String)?
|
||||||
|
|
||||||
@ -868,6 +923,9 @@ private final class NotificationServiceHandler {
|
|||||||
if let messageIdString = payloadJson["msg_id"] as? String {
|
if let messageIdString = payloadJson["msg_id"] as? String {
|
||||||
messageId = Int32(messageIdString)
|
messageId = Int32(messageIdString)
|
||||||
}
|
}
|
||||||
|
if let storyIdString = payloadJson["story_id"] as? String {
|
||||||
|
storyId = Int32(storyIdString)
|
||||||
|
}
|
||||||
|
|
||||||
if let fromIdString = payloadJson["from_id"] as? String {
|
if let fromIdString = payloadJson["from_id"] as? String {
|
||||||
if let userIdValue = Int64(fromIdString) {
|
if let userIdValue = Int64(fromIdString) {
|
||||||
@ -917,7 +975,9 @@ private final class NotificationServiceHandler {
|
|||||||
enum Action {
|
enum Action {
|
||||||
case logout
|
case logout
|
||||||
case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?)
|
case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?)
|
||||||
|
case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32)
|
||||||
case deleteMessage([MessageId])
|
case deleteMessage([MessageId])
|
||||||
|
case readReactions([MessageId])
|
||||||
case readMessage(MessageId)
|
case readMessage(MessageId)
|
||||||
case call(CallData)
|
case call(CallData)
|
||||||
}
|
}
|
||||||
@ -948,6 +1008,20 @@ private final class NotificationServiceHandler {
|
|||||||
action = .deleteMessage(messagesDeleted)
|
action = .deleteMessage(messagesDeleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "READ_REACTION":
|
||||||
|
if let peerId {
|
||||||
|
if let messageId = messageId {
|
||||||
|
action = .readReactions([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)])
|
||||||
|
} else if let messageIds = payloadJson["messages"] as? String {
|
||||||
|
var messages: [MessageId] = []
|
||||||
|
for messageId in messageIds.split(separator: ",") {
|
||||||
|
if let messageIdValue = Int32(messageId) {
|
||||||
|
messages.append(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action = .readReactions(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
case "READ_HISTORY":
|
case "READ_HISTORY":
|
||||||
if let peerId = peerId {
|
if let peerId = peerId {
|
||||||
if let messageIdString = payloadJson["max_id"] as? String {
|
if let messageIdString = payloadJson["max_id"] as? String {
|
||||||
@ -989,6 +1063,10 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
messageIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)
|
messageIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)
|
||||||
}
|
}
|
||||||
|
if let storyId = storyId {
|
||||||
|
interactionAuthorId = peerId
|
||||||
|
content.userInfo["story_id"] = "\(storyId)"
|
||||||
|
}
|
||||||
|
|
||||||
if peerId.namespace == Namespaces.Peer.CloudUser {
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
content.userInfo["from_id"] = "\(peerId.id._internalGetInt64Value())"
|
content.userInfo["from_id"] = "\(peerId.id._internalGetInt64Value())"
|
||||||
@ -1060,7 +1138,12 @@ private final class NotificationServiceHandler {
|
|||||||
} else {
|
} else {
|
||||||
content.category = category
|
content.category = category
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aps["r"] != nil || aps["react_emoji"] != nil {
|
||||||
|
content.category = "t"
|
||||||
|
} else if payloadJson["r"] != nil || payloadJson["react_emoji"] != nil {
|
||||||
|
content.category = "t"
|
||||||
|
}
|
||||||
|
|
||||||
let _ = messageId
|
let _ = messageId
|
||||||
|
|
||||||
@ -1087,7 +1170,11 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
action = .poll(peerId: peerId, content: content, messageId: messageIdValue)
|
if let storyId {
|
||||||
|
action = .pollStories(peerId: peerId, content: content, storyId: storyId)
|
||||||
|
} else {
|
||||||
|
action = .poll(peerId: peerId, content: content, messageId: messageIdValue)
|
||||||
|
}
|
||||||
|
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
}
|
}
|
||||||
@ -1194,6 +1281,7 @@ private final class NotificationServiceHandler {
|
|||||||
let collectedData = Atomic<DataValue>(value: DataValue())
|
let collectedData = Atomic<DataValue>(value: DataValue())
|
||||||
|
|
||||||
return standaloneMultipartFetch(
|
return standaloneMultipartFetch(
|
||||||
|
accountPeerId: stateManager.accountPeerId,
|
||||||
postbox: stateManager.postbox,
|
postbox: stateManager.postbox,
|
||||||
network: stateManager.network,
|
network: stateManager.network,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
@ -1284,6 +1372,7 @@ private final class NotificationServiceHandler {
|
|||||||
fetchNotificationSoundSignal = Signal { subscriber in
|
fetchNotificationSoundSignal = Signal { subscriber in
|
||||||
let collectedData = Atomic<Data>(value: Data())
|
let collectedData = Atomic<Data>(value: Data())
|
||||||
return standaloneMultipartFetch(
|
return standaloneMultipartFetch(
|
||||||
|
accountPeerId: stateManager.accountPeerId,
|
||||||
postbox: stateManager.postbox,
|
postbox: stateManager.postbox,
|
||||||
network: stateManager.network,
|
network: stateManager.network,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
@ -1492,7 +1581,7 @@ private final class NotificationServiceHandler {
|
|||||||
return true
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1527,6 +1616,253 @@ private final class NotificationServiceHandler {
|
|||||||
|> map { _ -> NotificationContent in }
|
|> map { _ -> NotificationContent in }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updatedContent = initialContent
|
||||||
|
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in
|
||||||
|
updatedContent = content
|
||||||
|
}, completed: {
|
||||||
|
pollCompletion(updatedContent)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
case let .pollStories(peerId, initialContent, storyId):
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will poll stories for \(peerId)")
|
||||||
|
if let stateManager = strongSelf.stateManager {
|
||||||
|
let pollCompletion: (NotificationContent) -> Void = { content in
|
||||||
|
let content = content
|
||||||
|
|
||||||
|
queue.async {
|
||||||
|
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
||||||
|
let content = NotificationContent(isLockedMessage: isLockedMessage)
|
||||||
|
updateCurrentContent(content)
|
||||||
|
completed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchStoriesSignal: Signal<Void, NoError> = .single(Void())
|
||||||
|
fetchStoriesSignal = _internal_pollPeerStories(postbox: stateManager.postbox, network: stateManager.network, accountPeerId: stateManager.accountPeerId, peerId: peerId)
|
||||||
|
|> map { _ -> Void in
|
||||||
|
}
|
||||||
|
|> then(
|
||||||
|
stateManager.postbox.transaction { transaction -> (MediaResourceReference, Int64?)? in
|
||||||
|
guard let state = transaction.getPeerStoryState(peerId: peerId)?.entry.get(Stories.PeerState.self) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let firstUnseenItem = transaction.getStoryItems(peerId: peerId).first(where: { entry in
|
||||||
|
return entry.id > state.maxReadId
|
||||||
|
})
|
||||||
|
guard let firstUnseenItem, firstUnseenItem.id == storyId else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let peer = transaction.getPeer(peerId).flatMap(PeerReference.init) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let storyItem = transaction.getStory(id: StoryId(peerId: peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = item.media {
|
||||||
|
var resource: MediaResource?
|
||||||
|
var fetchSize: Int64?
|
||||||
|
if let image = media as? TelegramMediaImage {
|
||||||
|
resource = largestImageRepresentation(image.representations)?.resource
|
||||||
|
} else if let file = media as? TelegramMediaFile {
|
||||||
|
resource = file.resource
|
||||||
|
for attribute in file.attributes {
|
||||||
|
if case let .Video(_, _, _, preloadSize) = attribute {
|
||||||
|
fetchSize = preloadSize.flatMap(Int64.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let resource else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (MediaResourceReference.media(media: .story(peer: peer, id: storyId, media: media), resource: resource), fetchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|> mapToSignal { resourceData -> Signal<Void, NoError> in
|
||||||
|
guard let (resource, _) = resourceData, let resourceValue = resource.resource as? TelegramMultipartFetchableResource else {
|
||||||
|
return .single(Void())
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)])
|
||||||
|
return Signal<Void, NoError> { subscriber in
|
||||||
|
let collectedData = Atomic<Data>(value: Data())
|
||||||
|
return standaloneMultipartFetch(
|
||||||
|
accountPeerId: stateManager.accountPeerId,
|
||||||
|
postbox: stateManager.postbox,
|
||||||
|
network: stateManager.network,
|
||||||
|
resource: resourceValue,
|
||||||
|
datacenterId: resourceValue.datacenterId,
|
||||||
|
size: nil,
|
||||||
|
intervals: intervals,
|
||||||
|
parameters: MediaResourceFetchParameters(
|
||||||
|
tag: nil,
|
||||||
|
info: resourceFetchInfo(reference: resource),
|
||||||
|
location: .init(peerId: peerId, messageId: nil),
|
||||||
|
contentType: .other,
|
||||||
|
isRandomAccessAllowed: true
|
||||||
|
),
|
||||||
|
encryptionKey: nil,
|
||||||
|
decryptedSize: nil,
|
||||||
|
continueInBackground: false,
|
||||||
|
useMainConnection: true
|
||||||
|
).start(next: { result in
|
||||||
|
switch result {
|
||||||
|
case let .dataPart(_, data, _, _):
|
||||||
|
let _ = collectedData.modify { current in
|
||||||
|
var current = current
|
||||||
|
current.append(data)
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, error: { _ in
|
||||||
|
subscriber.putNext(Void())
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}, completed: {
|
||||||
|
stateManager.postbox.mediaBox.storeResourceData(resource.resource.id, data: collectedData.with({ $0 }))
|
||||||
|
subscriber.putNext(Void())
|
||||||
|
subscriber.putCompletion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let fetchMediaSignal: Signal<Data?, NoError> = .single(nil)
|
||||||
|
|
||||||
|
var fetchNotificationSoundSignal: Signal<Data?, NoError> = .single(nil)
|
||||||
|
if let (downloadNotificationSound, _, _) = downloadNotificationSound {
|
||||||
|
var fetchResource: TelegramMultipartFetchableResource?
|
||||||
|
fetchResource = downloadNotificationSound.resource as? TelegramMultipartFetchableResource
|
||||||
|
|
||||||
|
if let resource = fetchResource {
|
||||||
|
if let path = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||||
|
fetchNotificationSoundSignal = .single(data)
|
||||||
|
} else {
|
||||||
|
let intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)])
|
||||||
|
fetchNotificationSoundSignal = Signal { subscriber in
|
||||||
|
let collectedData = Atomic<Data>(value: Data())
|
||||||
|
return standaloneMultipartFetch(
|
||||||
|
accountPeerId: stateManager.accountPeerId,
|
||||||
|
postbox: stateManager.postbox,
|
||||||
|
network: stateManager.network,
|
||||||
|
resource: resource,
|
||||||
|
datacenterId: resource.datacenterId,
|
||||||
|
size: nil,
|
||||||
|
intervals: intervals,
|
||||||
|
parameters: MediaResourceFetchParameters(
|
||||||
|
tag: nil,
|
||||||
|
info: resourceFetchInfo(resource: resource),
|
||||||
|
location: nil,
|
||||||
|
contentType: .other,
|
||||||
|
isRandomAccessAllowed: true
|
||||||
|
),
|
||||||
|
encryptionKey: nil,
|
||||||
|
decryptedSize: nil,
|
||||||
|
continueInBackground: false,
|
||||||
|
useMainConnection: true
|
||||||
|
).start(next: { result in
|
||||||
|
switch result {
|
||||||
|
case let .dataPart(_, data, _, _):
|
||||||
|
let _ = collectedData.modify { current in
|
||||||
|
var current = current
|
||||||
|
current.append(data)
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, error: { _ in
|
||||||
|
subscriber.putNext(nil)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putNext(collectedData.with({ $0 }))
|
||||||
|
subscriber.putCompletion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will fetch media")
|
||||||
|
let _ = (combineLatest(queue: queue,
|
||||||
|
fetchMediaSignal
|
||||||
|
|> timeout(10.0, queue: queue, alternate: .single(nil)),
|
||||||
|
fetchNotificationSoundSignal
|
||||||
|
|> timeout(10.0, queue: queue, alternate: .single(nil)),
|
||||||
|
fetchStoriesSignal
|
||||||
|
|> timeout(10.0, queue: queue, alternate: .single(Void()))
|
||||||
|
)
|
||||||
|
|> deliverOn(queue)).start(next: { mediaData, notificationSoundData, _ in
|
||||||
|
guard let strongSelf = self, let _ = strongSelf.stateManager else {
|
||||||
|
completed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Did fetch media \(mediaData == nil ? "Non-empty" : "Empty")")
|
||||||
|
|
||||||
|
if let notificationSoundData = notificationSoundData {
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Did fetch notificationSoundData")
|
||||||
|
|
||||||
|
if let (_, filePath, _) = downloadNotificationSound {
|
||||||
|
let _ = try? notificationSoundData.write(to: URL(fileURLWithPath: filePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
||||||
|
|
||||||
|
updateCurrentContent(content)
|
||||||
|
|
||||||
|
completed()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollSignal: Signal<Never, NoError>
|
||||||
|
pollSignal = .complete()
|
||||||
|
|
||||||
|
stateManager.network.shouldKeepConnection.set(.single(true))
|
||||||
|
|
||||||
|
let pollWithUpdatedContent: Signal<NotificationContent, NoError>
|
||||||
|
if interactionAuthorId != nil || messageId != nil {
|
||||||
|
pollWithUpdatedContent = stateManager.postbox.transaction { transaction -> NotificationContent in
|
||||||
|
var content = initialContent
|
||||||
|
|
||||||
|
if let interactionAuthorId = interactionAuthorId {
|
||||||
|
if inAppNotificationSettings.displayNameOnLockscreen, let peer = transaction.getPeer(interactionAuthorId) {
|
||||||
|
var foundLocalId: String?
|
||||||
|
transaction.enumerateDeviceContactImportInfoItems({ _, value in
|
||||||
|
if let value = value as? TelegramDeviceContactImportedData {
|
||||||
|
switch value {
|
||||||
|
case let .imported(data, _, peerId):
|
||||||
|
if peerId == interactionAuthorId {
|
||||||
|
foundLocalId = data.localIdentifiers.first
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, topicTitle: topicTitle, contactIdentifier: foundLocalId, isStory: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|> then(
|
||||||
|
pollSignal
|
||||||
|
|> map { _ -> NotificationContent in }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pollWithUpdatedContent = pollSignal
|
||||||
|
|> map { _ -> NotificationContent in }
|
||||||
|
}
|
||||||
|
|
||||||
var updatedContent = initialContent
|
var updatedContent = initialContent
|
||||||
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in
|
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in
|
||||||
updatedContent = content
|
updatedContent = content
|
||||||
@ -1588,6 +1924,45 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
case let .readReactions(ids):
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will read reactions \(ids)")
|
||||||
|
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
||||||
|
var removeIdentifiers: [String] = []
|
||||||
|
for notification in notifications {
|
||||||
|
if notification.request.content.categoryIdentifier != "t" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) {
|
||||||
|
for id in ids {
|
||||||
|
if PeerId(peerIdValue) == id.peerId && messageIdValue == id.id {
|
||||||
|
removeIdentifiers.append(notification.request.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let completeRemoval: () -> Void = {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var content = NotificationContent(isLockedMessage: nil)
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
||||||
|
|
||||||
|
updateCurrentContent(content)
|
||||||
|
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removeIdentifiers.isEmpty {
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications")
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
|
||||||
|
queue.after(1.0, {
|
||||||
|
completeRemoval()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
completeRemoval()
|
||||||
|
}
|
||||||
|
})
|
||||||
case let .readMessage(id):
|
case let .readMessage(id):
|
||||||
Logger.shared.log("NotificationService \(episode)", "Will read message \(id)")
|
Logger.shared.log("NotificationService \(episode)", "Will read message \(id)")
|
||||||
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
||||||
|
@ -36,7 +36,11 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
|
|||||||
|> mapToSignal { view -> Signal<[INMessage], NoError> in
|
|> mapToSignal { view -> Signal<[INMessage], NoError> in
|
||||||
var signals: [Signal<[INMessage], NoError>] = []
|
var signals: [Signal<[INMessage], NoError>] = []
|
||||||
for entry in view.0.entries {
|
for entry in view.0.entries {
|
||||||
if case let .MessageEntry(index, _, readState, isMuted, _, _, _, _, _, _, _, _, _) = entry {
|
if case let .MessageEntry(entryData) = entry {
|
||||||
|
let index = entryData.index
|
||||||
|
let readState = entryData.readState
|
||||||
|
let isMuted = entryData.isRemovedFromTotalUnreadCount
|
||||||
|
|
||||||
if index.messageIndex.id.peerId.namespace != Namespaces.Peer.CloudUser {
|
if index.messageIndex.id.peerId.namespace != Namespaces.Peer.CloudUser {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@objc(Application) class Application: UIApplication {
|
@objc(Application) class Application: UIApplication {
|
||||||
|
override func sendEvent(_ event: UIEvent) {
|
||||||
|
super.sendEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,6 +263,12 @@
|
|||||||
|
|
||||||
"PUSH_CHAT_REQ_JOINED" = "%2$@|%1$@ was accepted into the group";
|
"PUSH_CHAT_REQ_JOINED" = "%2$@|%1$@ was accepted into the group";
|
||||||
|
|
||||||
|
"PUSH_STORY_NOTEXT" = "%1$@|posted a story";
|
||||||
|
"PUSH_MESSAGE_STORY" = "%1$@|shared a story with you";
|
||||||
|
"PUSH_MESSAGE_STORY_MENTION" = "%1$@|mentioned you in a story";
|
||||||
|
"PUSH_CHANNEL_MESSAGE_STORY" = "%1$@|shared a story";
|
||||||
|
"PUSH_CHAT_MESSAGE_STORY" = "%2$@|%1$@ shared a story to the group";
|
||||||
|
|
||||||
"LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages";
|
"LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages";
|
||||||
"LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages";
|
"LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages";
|
||||||
"LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages";
|
"LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages";
|
||||||
@ -963,6 +969,8 @@
|
|||||||
"PrivacySettings.LastSeenContactsMinus" = "My Contacts (-%@)";
|
"PrivacySettings.LastSeenContactsMinus" = "My Contacts (-%@)";
|
||||||
"PrivacySettings.LastSeenContactsMinusPlus" = "My Contacts (-%@, +%@)";
|
"PrivacySettings.LastSeenContactsMinusPlus" = "My Contacts (-%@, +%@)";
|
||||||
"PrivacySettings.LastSeenNobodyPlus" = "Nobody (+%@)";
|
"PrivacySettings.LastSeenNobodyPlus" = "Nobody (+%@)";
|
||||||
|
"PrivacySettings.LastSeenCloseFriendsPlus" = "Close Friends (+%@)";
|
||||||
|
"PrivacySettings.LastSeenCloseFriends" = "Close Friends";
|
||||||
|
|
||||||
"PrivacySettings.SecurityTitle" = "SECURITY";
|
"PrivacySettings.SecurityTitle" = "SECURITY";
|
||||||
|
|
||||||
@ -2054,6 +2062,7 @@
|
|||||||
|
|
||||||
"StickerPack.Share" = "Share";
|
"StickerPack.Share" = "Share";
|
||||||
"StickerPack.Send" = "Send Sticker";
|
"StickerPack.Send" = "Send Sticker";
|
||||||
|
"StickerPack.AddSticker" = "Add Sticker";
|
||||||
|
|
||||||
"StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker";
|
"StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker";
|
||||||
"StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers";
|
"StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers";
|
||||||
@ -5823,6 +5832,8 @@ Sorry for the inconvenience.";
|
|||||||
"VoiceChat.Audio" = "audio";
|
"VoiceChat.Audio" = "audio";
|
||||||
"VoiceChat.Leave" = "leave";
|
"VoiceChat.Leave" = "leave";
|
||||||
|
|
||||||
|
"LiveStream.Expand" = "expand";
|
||||||
|
|
||||||
"VoiceChat.SpeakPermissionEveryone" = "New participants can speak";
|
"VoiceChat.SpeakPermissionEveryone" = "New participants can speak";
|
||||||
"VoiceChat.SpeakPermissionAdmin" = "New paricipants are muted";
|
"VoiceChat.SpeakPermissionAdmin" = "New paricipants are muted";
|
||||||
"VoiceChat.Share" = "Share Invite Link";
|
"VoiceChat.Share" = "Share Invite Link";
|
||||||
@ -5959,7 +5970,9 @@ Sorry for the inconvenience.";
|
|||||||
"LiveStream.RecordingInProgress" = "Live stream is being recorded";
|
"LiveStream.RecordingInProgress" = "Live stream is being recorded";
|
||||||
|
|
||||||
"VoiceChat.StopRecordingTitle" = "Stop Recording?";
|
"VoiceChat.StopRecordingTitle" = "Stop Recording?";
|
||||||
"VoiceChat.StopRecordingStop" = "Stop";
|
"VoiceChat.StopRecordingStop" = "Stop Recording";
|
||||||
|
|
||||||
|
"LiveStream.StopLiveStream" = "Stop Live Stream";
|
||||||
|
|
||||||
"VoiceChat.RecordingSaved" = "Audio saved to **Saved Messages**.";
|
"VoiceChat.RecordingSaved" = "Audio saved to **Saved Messages**.";
|
||||||
|
|
||||||
@ -6899,7 +6912,7 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
|
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
|
||||||
"SponsoredMessageInfoScreen.Title" = "What are sponsored messages?";
|
"SponsoredMessageInfoScreen.Title" = "What are sponsored messages?";
|
||||||
"SponsoredMessageInfoScreen.Text" = "Unlike other apps, Telegram never uses your private data to target ads. You are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
|
"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Telegram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
|
||||||
"SponsoredMessageInfo.Action" = "Learn More";
|
"SponsoredMessageInfo.Action" = "Learn More";
|
||||||
"SponsoredMessageInfo.Url" = "https://telegram.org/ads";
|
"SponsoredMessageInfo.Url" = "https://telegram.org/ads";
|
||||||
|
|
||||||
@ -7078,6 +7091,7 @@ Sorry for the inconvenience.";
|
|||||||
"Time.HoursAgo_many" = "%@ hours ago";
|
"Time.HoursAgo_many" = "%@ hours ago";
|
||||||
"Time.HoursAgo_0" = "%@ hours ago";
|
"Time.HoursAgo_0" = "%@ hours ago";
|
||||||
"Time.AtDate" = "%@";
|
"Time.AtDate" = "%@";
|
||||||
|
"Time.AtPreciseDate" = "%@ at %@";
|
||||||
|
|
||||||
"Stickers.ShowMore" = "Show More";
|
"Stickers.ShowMore" = "Show More";
|
||||||
|
|
||||||
@ -7420,6 +7434,7 @@ Sorry for the inconvenience.";
|
|||||||
"LiveStream.NoViewers" = "No viewers";
|
"LiveStream.NoViewers" = "No viewers";
|
||||||
"LiveStream.ViewerCount_1" = "1 viewer";
|
"LiveStream.ViewerCount_1" = "1 viewer";
|
||||||
"LiveStream.ViewerCount_any" = "%@ viewers";
|
"LiveStream.ViewerCount_any" = "%@ viewers";
|
||||||
|
"LiveStream.Watching" = "watching";
|
||||||
|
|
||||||
"LiveStream.NoSignalAdminText" = "Oops! Telegram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app.";
|
"LiveStream.NoSignalAdminText" = "Oops! Telegram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app.";
|
||||||
"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Telegram.";
|
"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Telegram.";
|
||||||
@ -7536,6 +7551,7 @@ Sorry for the inconvenience.";
|
|||||||
"PeerInfo.AutoDeleteSettingOther" = "Other...";
|
"PeerInfo.AutoDeleteSettingOther" = "Other...";
|
||||||
"PeerInfo.AutoDeleteDisable" = "Disable";
|
"PeerInfo.AutoDeleteDisable" = "Disable";
|
||||||
"PeerInfo.AutoDeleteInfo" = "Automatically delete messages sent in this chat after a certain period of time.";
|
"PeerInfo.AutoDeleteInfo" = "Automatically delete messages sent in this chat after a certain period of time.";
|
||||||
|
"PeerInfo.ChannelAutoDeleteInfo" = "Automatically delete messages sent in this channel after a certain period of time.";
|
||||||
|
|
||||||
"PeerInfo.ClearMessages" = "Clear Messages";
|
"PeerInfo.ClearMessages" = "Clear Messages";
|
||||||
"PeerInfo.ClearConfirmationUser" = "Are you sure you want to delete all messages with %@?";
|
"PeerInfo.ClearConfirmationUser" = "Are you sure you want to delete all messages with %@?";
|
||||||
@ -9106,11 +9122,6 @@ Sorry for the inconvenience.";
|
|||||||
"Wallpaper.ApplyForAll" = "Apply For All Chats";
|
"Wallpaper.ApplyForAll" = "Apply For All Chats";
|
||||||
"Wallpaper.ApplyForChat" = "Apply For This Chat";
|
"Wallpaper.ApplyForChat" = "Apply For This Chat";
|
||||||
|
|
||||||
"ChatList.ChatFolderUpdateCount_1" = "1 new chat";
|
|
||||||
"ChatList.ChatFolderUpdateCount_any" = "%d new chats";
|
|
||||||
"ChatList.ChatFolderUpdateHintTitle" = "You can join %@";
|
|
||||||
"ChatList.ChatFolderUpdateHintText" = "Tap here to view them";
|
|
||||||
|
|
||||||
"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **Telegram Premium** to increase this limit up to **%2$@**.";
|
"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **Telegram Premium** to increase this limit up to **%2$@**.";
|
||||||
"Premium.MaxSharedFolderMembershipNoPremiumText" = "You can only add **%1$@** shareable folders. We are working to let you increase this limit in the future.";
|
"Premium.MaxSharedFolderMembershipNoPremiumText" = "You can only add **%1$@** shareable folders. We are working to let you increase this limit in the future.";
|
||||||
"Premium.MaxSharedFolderMembershipFinalText" = "Sorry, you can only add **%1$@** shareable folders.";
|
"Premium.MaxSharedFolderMembershipFinalText" = "Sorry, you can only add **%1$@** shareable folders.";
|
||||||
@ -9342,3 +9353,240 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off";
|
"ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off";
|
||||||
"ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it.";
|
"ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it.";
|
||||||
|
|
||||||
|
"Notification.LockScreenReactionPlaceholder" = "Reaction";
|
||||||
|
|
||||||
|
"UserInfo.BotNamePlaceholder" = "Bot Name";
|
||||||
|
|
||||||
|
"ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off";
|
||||||
|
"ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it.";
|
||||||
|
|
||||||
|
"Login.ErrorAppOutdated" = "Please update Telegram to the latest version to log in.";
|
||||||
|
|
||||||
|
"Login.GetCodeViaFragment" = "Get a code via Fragment";
|
||||||
|
|
||||||
|
"Privacy.Bio" = "Bio";
|
||||||
|
"Privacy.Bio.WhoCanSeeMyBio" = "WHO CAN SEE MY BIO";
|
||||||
|
"Privacy.Bio.CustomHelp" = "You can restrict who can see your profile bio with granular precision.";
|
||||||
|
"Privacy.Bio.AlwaysShareWith.Title" = "Always Share With";
|
||||||
|
"Privacy.Bio.NeverShareWith.Title" = "Never Share With";
|
||||||
|
|
||||||
|
"Conversation.OpenLink" = "OPEN LINK";
|
||||||
|
|
||||||
|
"Paint.Flip" = "Flip";
|
||||||
|
|
||||||
|
"Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@";
|
||||||
|
"Message.ForwardedExpiredStoryShort" = "Expired Story\nFrom: %@";
|
||||||
|
|
||||||
|
"Conversation.StoryForwardTooltip.Chat.One" = "Story forwarded to **%@**";
|
||||||
|
"Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**";
|
||||||
|
"Conversation.StoryForwardTooltip.ManyChats.One" = "Story forwarded to to **%@** and %@ others";
|
||||||
|
"Conversation.StoryForwardTooltip.SavedMessages.One" = "Story forwarded to to **Saved Messages**";
|
||||||
|
|
||||||
|
"Conversation.StoryMentionTextOutgoing" = "You mentioned %@\nin a story";
|
||||||
|
"Conversation.StoryMentionTextIncoming" = "%@ mentioned you\nin a story";
|
||||||
|
"Conversation.StoryExpiredMentionTextOutgoing" = "The story where you mentioned %@\n is no longer available";
|
||||||
|
"Conversation.StoryExpiredMentionTextIncoming" = "The story you were mentioned in\nis no longer available";
|
||||||
|
|
||||||
|
"ChatList.ArchiveStoryCount_1" = "1 story";
|
||||||
|
"ChatList.ArchiveStoryCount_any" = "%d stories";
|
||||||
|
|
||||||
|
"Notification.Story" = "Story";
|
||||||
|
|
||||||
|
"ChatList.StoryFeedTooltip" = "Tap above to view updates\nfrom %@";
|
||||||
|
|
||||||
|
"StoryFeed.ContextAddStory" = "Add Story";
|
||||||
|
"StoryFeed.ContextSavedStories" = "Saved Stories";
|
||||||
|
"StoryFeed.ContextArchivedStories" = "Archived Stories";
|
||||||
|
"StoryFeed.ContextOpenChat" = "Send Message";
|
||||||
|
"StoryFeed.ContextOpenProfile" = "View Profile";
|
||||||
|
"StoryFeed.ContextNotifyOn" = "Notify About Stories";
|
||||||
|
"StoryFeed.ContextNotifyOff" = "Do Not Notify About Stories";
|
||||||
|
"StoryFeed.ContextArchive" = "Hide Stories";
|
||||||
|
"StoryFeed.ContextUnarchive" = "Unhide Stories";
|
||||||
|
|
||||||
|
"StoryFeed.TooltipNotifyOn" = "You will now get a notification whenever **%@** posts a story.";
|
||||||
|
"StoryFeed.TooltipNotifyOff" = "You will no longer receive a notification when **%@** posts a story.";
|
||||||
|
"StoryFeed.TooltipArchive" = "Stories from **%@** will now be shown in Archived Chats.";
|
||||||
|
"StoryFeed.TooltipUnarchive" = "Stories from **%@** will now be shown in Chats.";
|
||||||
|
|
||||||
|
"ChatList.Archive.ContextSettings" = "Archive Settings";
|
||||||
|
"ChatList.Archive.ContextInfo" = "How Does It Work?";
|
||||||
|
"ChatList.ContextSelectChats" = "Select Chats";
|
||||||
|
|
||||||
|
"StoryFeed.TooltipPremiumPosting" = "Posting stories is currently available only\nto subscribers of [Telegram Premium]().";
|
||||||
|
"StoryFeed.TooltipStoryLimitValue_1" = "1 story";
|
||||||
|
"StoryFeed.TooltipStoryLimitValue_any" = "%d stories";
|
||||||
|
"StoryFeed.TooltipStoryLimit" = "You can't post more than **%@** stories in **24 hours**.";
|
||||||
|
|
||||||
|
"StoryFeed.MyStory" = "My Story";
|
||||||
|
"StoryFeed.MyUploading" = "Uploading...";
|
||||||
|
|
||||||
|
"MediaPicker.AddImage" = "Add Image";
|
||||||
|
|
||||||
|
"Premium.Stories" = "Story Posting";
|
||||||
|
"Premium.StoriesInfo" = "Be one of the first to share your stories with your contacts or an unlimited audience.";
|
||||||
|
"Premium.Stories.Proceed" = "Unlock Story Posting";
|
||||||
|
|
||||||
|
"AutoDownloadSettings.OnForContacts" = "On for contacts";
|
||||||
|
|
||||||
|
"AutoDownloadSettings.StoriesSectionHeader" = "AUTO-DOWNLOAD STORIES";
|
||||||
|
"AutoDownloadSettings.StoriesArchivedContacts" = "Archived Contacts";
|
||||||
|
|
||||||
|
"AutoDownloadSettings.StoriesTitle" = "Stories";
|
||||||
|
|
||||||
|
"Notifications.TopChats" = "Top 5";
|
||||||
|
"Notifications.Stories" = "Stories";
|
||||||
|
|
||||||
|
"Settings.MyStories" = "My Stories";
|
||||||
|
"Settings.StoriesArchive" = "Stories Archive";
|
||||||
|
|
||||||
|
"ArchiveSettings.Title" = "Archive Settings";
|
||||||
|
|
||||||
|
"ArchiveSettings.UnmutedChatsHeader" = "UNMUTED CHATS";
|
||||||
|
"ArchiveSettings.UnmutedChatsFooter" = "Keep archived chats in the Archive even if they are unmuted and get a new message.";
|
||||||
|
"ArchiveSettings.FolderChatsHeader" = "CHATS FROM FOLDERS";
|
||||||
|
"ArchiveSettings.FolderChatsFooter" = "Keep archived chats from folders in the Archive even if they are unmuted and get a new message.";
|
||||||
|
|
||||||
|
"ArchiveSettings.UnknownChatsHeader" = "NEW CHATS FROM UNKNOWN USERS";
|
||||||
|
"ArchiveSettings.UnknownChatsFooter" = "Automatically archive and mute new private chats, groups and channels from non-contacts.";
|
||||||
|
|
||||||
|
"ArchiveSettings.KeepArchived" = "Always Keep Archived";
|
||||||
|
"ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [Telegram Premium]().";
|
||||||
|
|
||||||
|
"NotificationSettings.Stories.ShowAll" = "Show All Notifications";
|
||||||
|
"NotificationSettings.Stories.ShowImportant" = "Show Important Notifications";
|
||||||
|
"NotificationSettings.Stories.ShowImportantFooter" = "Always on for top 5 contacts.";
|
||||||
|
"NotificationSettings.Stories.DisplayAuthorName" = "Display Author Name";
|
||||||
|
|
||||||
|
"NotificationSettings.Stories.AutomaticValue" = "%@ (automatic)";
|
||||||
|
"NotificationSettings.Stories.CompactShowName" = "Show name";
|
||||||
|
"NotificationSettings.Stories.CompactHideName" = "Hide name";
|
||||||
|
|
||||||
|
"Notifications.StoriesTitle" = "Stories";
|
||||||
|
|
||||||
|
"Message.Story" = "Story";
|
||||||
|
|
||||||
|
"Notification.Exceptions.StoriesHeader" = "STORY NOTIFICATIONS";
|
||||||
|
"Notification.Exceptions.StoriesDisplayAuthorName" = "DISPLAY AUTHOR NAME";
|
||||||
|
|
||||||
|
"StorageManagement.SectionStories" = "Stories";
|
||||||
|
|
||||||
|
"PeerInfo.PaneStories" = "Stories";
|
||||||
|
|
||||||
|
"Story.TooltipExpired" = "This story is no longer available";
|
||||||
|
|
||||||
|
"Chat.ReplyExpiredStory" = "Expired story";
|
||||||
|
"Chat.ReplyStory" = "Story";
|
||||||
|
|
||||||
|
"Chat.StoryMentionAction" = "View Story";
|
||||||
|
|
||||||
|
"StoryList.ContextSaveToGallery" = "Save to Gallery";
|
||||||
|
"StoryList.ContextShowArchive" = "Show Archive";
|
||||||
|
|
||||||
|
"StoryList.TooltipStoriesDeleted_1" = "1 story deleted.";
|
||||||
|
"StoryList.TooltipStoriesDeleted_any" = "%d stories deleted.";
|
||||||
|
|
||||||
|
"Story.TooltipSaving" = "Saving";
|
||||||
|
"Story.TooltipSaved" = "Saved";
|
||||||
|
|
||||||
|
"StoryList.SaveToProfile" = "Save to Profile";
|
||||||
|
"StoryList.TooltipStoriesSavedToProfile_1" = "Story saved to your profile";
|
||||||
|
"StoryList.TooltipStoriesSavedToProfile_any" = "%d stories saved to your profile.";
|
||||||
|
"StoryList.TooltipStoriesSavedToProfileText" = "Saved stories can be viewed by others on your profile until you remove them.";
|
||||||
|
|
||||||
|
"StoryList.TitleSaved" = "My Stories";
|
||||||
|
"StoryList.TitleArchive" = "Stories Archive";
|
||||||
|
"StoryList.SubtitleSelected_1" = "1 story selected";
|
||||||
|
"StoryList.SubtitleSelected_any" = "%d stories selected";
|
||||||
|
|
||||||
|
"StoryList.SubtitleSaved_1" = "1 saved story";
|
||||||
|
"StoryList.SubtitleSaved_any" = "%d saved stories";
|
||||||
|
|
||||||
|
"StoryList.SubtitleCount_1" = "1 story";
|
||||||
|
"StoryList.SubtitleCount_any" = "%d stories";
|
||||||
|
|
||||||
|
"StoryList.ArchiveDescription" = "Only you can see archived stories unless you choose to save them to your profile.";
|
||||||
|
|
||||||
|
"StoryList.SavedEmptyState.Title" = "No saved stories";
|
||||||
|
"StoryList.SavedEmptyState.Text" = "Open the Archive to select stories you\nwant to be displayed in your profile.";
|
||||||
|
"StoryList.ArchivedEmptyState.Title" = "No Archived Stories";
|
||||||
|
"StoryList.ArchivedEmptyState.Text" = "Upload a new story to view it here";
|
||||||
|
"StoryList.SavedEmptyAction" = "Open Archive";
|
||||||
|
|
||||||
|
"ArchiveInfo.Title" = "This is Your Archive";
|
||||||
|
|
||||||
|
"ArchiveInfo.TextKeepArchivedUnmuted" = "Archived chats will remain in the Archive when you receive a new message. [Tap to change >]()";
|
||||||
|
"ArchiveInfo.TextKeepArchivedDefault" = "When you receive a new message, muted chats will remain in the Archive, while unmuted chats will be moved to Chats. [Tap to change >]()";
|
||||||
|
|
||||||
|
"ArchiveInfo.ChatsTitle" = "Archived Chats";
|
||||||
|
"ArchiveInfo.ChatsText" = "Move any chat into your Archive and back by swiping on it.";
|
||||||
|
"ArchiveInfo.HideTitle" = "Hiding Archive";
|
||||||
|
"ArchiveInfo.HideText" = "Hide the Archive from your Main screen by swiping on it.";
|
||||||
|
"ArchiveInfo.StoriesTitle" = "Stories";
|
||||||
|
"ArchiveInfo.StoriesText" = "Archive Stories from your contacts separately from chats with them.";
|
||||||
|
"ArchiveInfo.CloseAction" = "Got it";
|
||||||
|
|
||||||
|
"Story.HeaderYourStory" = "Your story";
|
||||||
|
"Story.HeaderEdited" = "edited";
|
||||||
|
"Story.CaptionShowMore" = "Show more";
|
||||||
|
|
||||||
|
"Story.UnsupportedText" = "This story is not supported by\nyour version of Telegram.";
|
||||||
|
"Story.UnsupportedAction" = "Update Telegram";
|
||||||
|
|
||||||
|
"Story.ScreenshotBlockedTitle" = "Screenshot Blocked";
|
||||||
|
"Story.ScreenshotBlockedText" = "The story you tried to take a\nscreenshot of is protected from\ncopying by its creator.";
|
||||||
|
|
||||||
|
"Story.Footer.NoViews" = "No views";
|
||||||
|
"Story.Footer.Views_1" = "1 view";
|
||||||
|
"Story.Footer.Views_any" = "%d views";
|
||||||
|
|
||||||
|
"Story.Footer.Uploading" = "Uploading...";
|
||||||
|
|
||||||
|
"Story.FooterReplyUnavailable" = "You can't reply to this story";
|
||||||
|
"Story.InputPlaceholderReplyPrivately" = "Reply Privately...";
|
||||||
|
|
||||||
|
"Story.ContextDeleteStory" = "Delete Story";
|
||||||
|
|
||||||
|
"Story.TooltipPrivacyCloseFriendsMy" = "Only people from your close friends list will see this story.";
|
||||||
|
"Story.TooltipPrivacyCloseFriends" = "You are seeing this story because you have\nbeen added to %@'s list of close friends.";
|
||||||
|
|
||||||
|
"Story.ToastViewInChat" = "View in Chat";
|
||||||
|
"Story.ToastReactionSent" = "Reaction Sent.";
|
||||||
|
|
||||||
|
"Story.PrivacyTooltipContacts" = "This story is shown to all your contacts.";
|
||||||
|
"Story.PrivacyTooltipCloseFriends" = "This story is shown to your close friends.";
|
||||||
|
"Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts.";
|
||||||
|
"Story.PrivacyTooltipNobody" = "This story is shown only to you.";
|
||||||
|
"Story.PrivacyTooltipEveryone" = "This story is shown to everyone.";
|
||||||
|
|
||||||
|
"Story.ContextPrivacy.LabelCloseFriends" = "Close Friends";
|
||||||
|
"Story.ContextPrivacy.LabelContactsExcept" = "Contacts (-%@)";
|
||||||
|
"Story.ContextPrivacy.LabelContacts" = "Contacts";
|
||||||
|
"Story.ContextPrivacy.LabelOnlySelected_1" = "1 Person";
|
||||||
|
"Story.ContextPrivacy.LabelOnlySelected_any" = "%d People";
|
||||||
|
"Story.ContextPrivacy.LabelOnlyMe" = "Only Me";
|
||||||
|
"Story.ContextPrivacy.LabelEveryone" = "Everyone";
|
||||||
|
"Story.Context.Privacy" = "Who Can See";
|
||||||
|
"Story.Context.Edit" = "Edit Story";
|
||||||
|
"Story.Context.SaveToProfile" = "Save to Profile";
|
||||||
|
"Story.Context.RemoveFromProfile" = "Remove from Profile";
|
||||||
|
"Story.ToastRemovedFromProfileText" = "Story removed from your profile";
|
||||||
|
"Story.ToastSavedToProfileTitle" = "Story saved to your profile";
|
||||||
|
"Story.ToastSavedToProfileText" = "Saved stories can be viewed by others on your profile until you remove them.";
|
||||||
|
"Story.Context.SaveToGallery" = "Save to Gallery";
|
||||||
|
"Story.Context.CopyLink" = "Copy Link";
|
||||||
|
"Story.ToastLinkCopied" = "Link copied.";
|
||||||
|
"Story.Context.Share" = "Share";
|
||||||
|
"Story.Context.Report" = "Report";
|
||||||
|
|
||||||
|
"Story.Context.EmbeddedStickersValue_1" = "1 pack";
|
||||||
|
"Story.Context.EmbeddedStickersValue_any" = "%d packs";
|
||||||
|
"Story.Context.EmbeddedStickers" = "This story contains stickers from [%@]().";
|
||||||
|
"Story.Context.EmbeddedEmojiPack" = "This story contains\n#[%@]() emoji.";
|
||||||
|
"Story.Context.EmbeddedStickerPack" = "This story contains\n#[%@]() stickers.";
|
||||||
|
|
||||||
|
"Story.TooltipVideoHasNoSound" = "This video has no sound";
|
||||||
|
|
||||||
|
"Story.TooltipMessageScheduled" = "Message Scheduled";
|
||||||
|
"Story.TooltipMessageSent" = "Message Sent";
|
||||||
|
@ -20,7 +20,6 @@ swift_library(
|
|||||||
"//submodules/Postbox:Postbox",
|
"//submodules/Postbox:Postbox",
|
||||||
"//submodules/TelegramCore:TelegramCore",
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||||
"//submodules/MeshAnimationCache:MeshAnimationCache",
|
|
||||||
"//submodules/Utils/RangeSet:RangeSet",
|
"//submodules/Utils/RangeSet:RangeSet",
|
||||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||||
"//submodules/TextFormat:TextFormat",
|
"//submodules/TextFormat:TextFormat",
|
||||||
|
@ -10,10 +10,10 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import DeviceLocationManager
|
import DeviceLocationManager
|
||||||
import TemporaryCachedPeerDataManager
|
import TemporaryCachedPeerDataManager
|
||||||
import MeshAnimationCache
|
|
||||||
import InAppPurchaseManager
|
import InAppPurchaseManager
|
||||||
import AnimationCache
|
import AnimationCache
|
||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
|
import Photos
|
||||||
|
|
||||||
public final class TelegramApplicationOpenUrlCompletion {
|
public final class TelegramApplicationOpenUrlCompletion {
|
||||||
public let completion: (Bool) -> Void
|
public let completion: (Bool) -> Void
|
||||||
@ -299,6 +299,7 @@ public enum ResolvedUrl {
|
|||||||
case invoice(slug: String, invoice: TelegramMediaInvoice?)
|
case invoice(slug: String, invoice: TelegramMediaInvoice?)
|
||||||
case premiumOffer(reference: String?)
|
case premiumOffer(reference: String?)
|
||||||
case chatFolder(slug: String)
|
case chatFolder(slug: String)
|
||||||
|
case story(peerId: PeerId, id: Int32)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NavigateToChatKeepStack {
|
public enum NavigateToChatKeepStack {
|
||||||
@ -465,8 +466,9 @@ public final class NavigateToChatControllerParams {
|
|||||||
public let changeColors: Bool
|
public let changeColors: Bool
|
||||||
public let setupController: (ChatController) -> Void
|
public let setupController: (ChatController) -> Void
|
||||||
public let completion: (ChatController) -> Void
|
public let completion: (ChatController) -> Void
|
||||||
|
public let pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)?
|
||||||
|
|
||||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) {
|
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||||
self.navigationController = navigationController
|
self.navigationController = navigationController
|
||||||
self.chatController = chatController
|
self.chatController = chatController
|
||||||
self.chatLocationContextHolder = chatLocationContextHolder
|
self.chatLocationContextHolder = chatLocationContextHolder
|
||||||
@ -494,6 +496,7 @@ public final class NavigateToChatControllerParams {
|
|||||||
self.chatNavigationStack = chatNavigationStack
|
self.chatNavigationStack = chatNavigationStack
|
||||||
self.changeColors = changeColors
|
self.changeColors = changeColors
|
||||||
self.setupController = setupController
|
self.setupController = setupController
|
||||||
|
self.pushController = pushController
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -739,6 +742,65 @@ public protocol AppLockContext: AnyObject {
|
|||||||
public protocol RecentSessionsController: AnyObject {
|
public protocol RecentSessionsController: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol AttachmentFileController: AnyObject {
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct StoryCameraTransitionIn {
|
||||||
|
public weak var sourceView: UIView?
|
||||||
|
public let sourceRect: CGRect
|
||||||
|
public let sourceCornerRadius: CGFloat
|
||||||
|
|
||||||
|
public init(
|
||||||
|
sourceView: UIView,
|
||||||
|
sourceRect: CGRect,
|
||||||
|
sourceCornerRadius: CGFloat
|
||||||
|
) {
|
||||||
|
self.sourceView = sourceView
|
||||||
|
self.sourceRect = sourceRect
|
||||||
|
self.sourceCornerRadius = sourceCornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct StoryCameraTransitionOut {
|
||||||
|
public weak var destinationView: UIView?
|
||||||
|
public let destinationRect: CGRect
|
||||||
|
public let destinationCornerRadius: CGFloat
|
||||||
|
|
||||||
|
public init(
|
||||||
|
destinationView: UIView,
|
||||||
|
destinationRect: CGRect,
|
||||||
|
destinationCornerRadius: CGFloat
|
||||||
|
) {
|
||||||
|
self.destinationView = destinationView
|
||||||
|
self.destinationRect = destinationRect
|
||||||
|
self.destinationCornerRadius = destinationCornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct StoryCameraTransitionInCoordinator {
|
||||||
|
public let animateIn: () -> Void
|
||||||
|
public let updateTransitionProgress: (CGFloat) -> Void
|
||||||
|
public let completeWithTransitionProgressAndVelocity: (CGFloat, CGFloat) -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
animateIn: @escaping () -> Void,
|
||||||
|
updateTransitionProgress: @escaping (CGFloat) -> Void,
|
||||||
|
completeWithTransitionProgressAndVelocity: @escaping (CGFloat, CGFloat) -> Void
|
||||||
|
) {
|
||||||
|
self.animateIn = animateIn
|
||||||
|
self.updateTransitionProgress = updateTransitionProgress
|
||||||
|
self.completeWithTransitionProgressAndVelocity = completeWithTransitionProgressAndVelocity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol TelegramRootControllerInterface: NavigationController {
|
||||||
|
@discardableResult
|
||||||
|
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
|
||||||
|
|
||||||
|
func getContactsController() -> ViewController?
|
||||||
|
func getChatsController() -> ViewController?
|
||||||
|
}
|
||||||
|
|
||||||
public protocol SharedAccountContext: AnyObject {
|
public protocol SharedAccountContext: AnyObject {
|
||||||
var sharedContainerPath: String { get }
|
var sharedContainerPath: String { get }
|
||||||
var basePath: String { get }
|
var basePath: String { get }
|
||||||
@ -806,6 +868,11 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
||||||
func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController
|
func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController
|
||||||
func makeStorageManagementController(context: AccountContext) -> ViewController
|
func makeStorageManagementController(context: AccountContext) -> ViewController
|
||||||
|
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController
|
||||||
|
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
|
||||||
|
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
|
||||||
|
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
|
||||||
|
func makeArchiveSettingsController(context: AccountContext) -> ViewController
|
||||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
|
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
|
||||||
@ -832,10 +899,14 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, action: @escaping () -> Void) -> ViewController
|
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, action: @escaping () -> Void) -> ViewController
|
||||||
|
|
||||||
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
|
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
|
||||||
|
|
||||||
|
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController
|
||||||
|
|
||||||
|
func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||||
|
|
||||||
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||||
|
|
||||||
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode) -> ViewController
|
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController
|
||||||
|
|
||||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||||
|
|
||||||
@ -843,6 +914,9 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
var hasOngoingCall: ValuePromise<Bool> { get }
|
var hasOngoingCall: ValuePromise<Bool> { get }
|
||||||
var immediateHasOngoingCall: Bool { get }
|
var immediateHasOngoingCall: Bool { get }
|
||||||
|
|
||||||
|
var enablePreloads: Promise<Bool> { get }
|
||||||
|
var hasPreloadBlockingContent: Promise<Bool> { get }
|
||||||
|
|
||||||
var hasGroupCallOnScreen: Signal<Bool, NoError> { get }
|
var hasGroupCallOnScreen: Signal<Bool, NoError> { get }
|
||||||
var currentGroupCallController: ViewController? { get }
|
var currentGroupCallController: ViewController? { get }
|
||||||
|
|
||||||
@ -872,6 +946,7 @@ public enum PremiumIntroSource {
|
|||||||
case voiceToText
|
case voiceToText
|
||||||
case fasterDownload
|
case fasterDownload
|
||||||
case translation
|
case translation
|
||||||
|
case stories
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PremiumDemoSubject {
|
public enum PremiumDemoSubject {
|
||||||
@ -889,6 +964,7 @@ public enum PremiumDemoSubject {
|
|||||||
case animatedEmoji
|
case animatedEmoji
|
||||||
case emojiStatus
|
case emojiStatus
|
||||||
case translation
|
case translation
|
||||||
|
case stories
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PremiumLimitSubject {
|
public enum PremiumLimitSubject {
|
||||||
@ -935,7 +1011,6 @@ public protocol AccountContext: AnyObject {
|
|||||||
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
|
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
|
||||||
|
|
||||||
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
||||||
var meshAnimationCache: MeshAnimationCache { get }
|
|
||||||
|
|
||||||
var animationCache: AnimationCache { get }
|
var animationCache: AnimationCache { get }
|
||||||
var animationRenderer: MultiAnimationRenderer { get }
|
var animationRenderer: MultiAnimationRenderer { get }
|
||||||
@ -1006,3 +1081,58 @@ public struct AntiSpamBotConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StoriesConfiguration {
|
||||||
|
public enum PostingAvailability {
|
||||||
|
case enabled
|
||||||
|
case premium
|
||||||
|
case disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
static var defaultValue: StoriesConfiguration {
|
||||||
|
return StoriesConfiguration(posting: .disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
public let posting: PostingAvailability
|
||||||
|
|
||||||
|
fileprivate init(posting: PostingAvailability) {
|
||||||
|
self.posting = posting
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func with(appConfiguration: AppConfiguration) -> StoriesConfiguration {
|
||||||
|
if let data = appConfiguration.data, let postingString = data["stories_posting"] as? String {
|
||||||
|
var posting: PostingAvailability
|
||||||
|
switch postingString {
|
||||||
|
case "enabled":
|
||||||
|
posting = .enabled
|
||||||
|
case "premium":
|
||||||
|
posting = .premium
|
||||||
|
default:
|
||||||
|
posting = .disabled
|
||||||
|
}
|
||||||
|
return StoriesConfiguration(posting: posting)
|
||||||
|
} else {
|
||||||
|
return .defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct StickersSearchConfiguration {
|
||||||
|
static var defaultValue: StickersSearchConfiguration {
|
||||||
|
return StickersSearchConfiguration(disableLocalSuggestions: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public let disableLocalSuggestions: Bool
|
||||||
|
|
||||||
|
fileprivate init(disableLocalSuggestions: Bool) {
|
||||||
|
self.disableLocalSuggestions = disableLocalSuggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration {
|
||||||
|
if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool {
|
||||||
|
return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi)
|
||||||
|
} else {
|
||||||
|
return .defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Postbox
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import Postbox
|
|
||||||
|
|
||||||
public final class ChatMessageItemAssociatedData: Equatable {
|
public final class ChatMessageItemAssociatedData: Equatable {
|
||||||
public enum ChannelDiscussionGroupStatus: Equatable {
|
public enum ChannelDiscussionGroupStatus: Equatable {
|
||||||
@ -49,8 +49,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
public let topicAuthorId: EnginePeer.Id?
|
public let topicAuthorId: EnginePeer.Id?
|
||||||
public let hasBots: Bool
|
public let hasBots: Bool
|
||||||
public let translateToLanguage: String?
|
public let translateToLanguage: String?
|
||||||
|
public let maxReadStoryId: Int32?
|
||||||
|
|
||||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false), topicAuthorId: EnginePeer.Id? = nil, hasBots: Bool = false, translateToLanguage: String? = nil) {
|
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false), topicAuthorId: EnginePeer.Id? = nil, hasBots: Bool = false, translateToLanguage: String? = nil, maxReadStoryId: Int32? = nil) {
|
||||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||||
@ -72,6 +73,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton
|
self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton
|
||||||
self.hasBots = hasBots
|
self.hasBots = hasBots
|
||||||
self.translateToLanguage = translateToLanguage
|
self.translateToLanguage = translateToLanguage
|
||||||
|
self.maxReadStoryId = maxReadStoryId
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||||
@ -135,6 +137,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
if lhs.translateToLanguage != rhs.translateToLanguage {
|
if lhs.translateToLanguage != rhs.translateToLanguage {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.maxReadStoryId != rhs.maxReadStoryId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,11 +202,11 @@ public struct ChatControllerInitialBotStart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatControllerInitialAttachBotStart {
|
public struct ChatControllerInitialAttachBotStart {
|
||||||
public let botId: PeerId
|
public let botId: EnginePeer.Id
|
||||||
public let payload: String?
|
public let payload: String?
|
||||||
public let justInstalled: Bool
|
public let justInstalled: Bool
|
||||||
|
|
||||||
public init(botId: PeerId, payload: String?, justInstalled: Bool) {
|
public init(botId: EnginePeer.Id, payload: String?, justInstalled: Bool) {
|
||||||
self.botId = botId
|
self.botId = botId
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.justInstalled = justInstalled
|
self.justInstalled = justInstalled
|
||||||
@ -313,6 +318,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
case textMention(EnginePeer.Id)
|
case textMention(EnginePeer.Id)
|
||||||
case textUrl(String)
|
case textUrl(String)
|
||||||
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
|
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
|
||||||
|
case strikethrough
|
||||||
|
case underline
|
||||||
|
case spoiler
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
@ -334,6 +342,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
|
let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
|
||||||
let fileId = try container.decode(Int64.self, forKey: "f")
|
let fileId = try container.decode(Int64.self, forKey: "f")
|
||||||
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
|
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
|
||||||
|
case 6:
|
||||||
|
self = .strikethrough
|
||||||
|
case 7:
|
||||||
|
self = .underline
|
||||||
|
case 8:
|
||||||
|
self = .spoiler
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
self = .bold
|
self = .bold
|
||||||
@ -359,6 +373,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
try container.encode(5 as Int32, forKey: "t")
|
try container.encode(5 as Int32, forKey: "t")
|
||||||
try container.encodeIfPresent(stickerPack, forKey: "s")
|
try container.encodeIfPresent(stickerPack, forKey: "s")
|
||||||
try container.encode(fileId, forKey: "f")
|
try container.encode(fileId, forKey: "f")
|
||||||
|
case .strikethrough:
|
||||||
|
try container.encode(6 as Int32, forKey: "t")
|
||||||
|
case .underline:
|
||||||
|
try container.encode(7 as Int32, forKey: "t")
|
||||||
|
case .spoiler:
|
||||||
|
try container.encode(8 as Int32, forKey: "t")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,6 +446,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
|||||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
|
||||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
|
||||||
|
} else if key == ChatTextInputAttributes.strikethrough {
|
||||||
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length)))
|
||||||
|
} else if key == ChatTextInputAttributes.underline {
|
||||||
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
|
||||||
|
} else if key == ChatTextInputAttributes.spoiler {
|
||||||
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -464,6 +490,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
|||||||
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
case let .customEmoji(_, fileId):
|
case let .customEmoji(_, fileId):
|
||||||
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
|
case .strikethrough:
|
||||||
|
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
|
case .underline:
|
||||||
|
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
|
case .spoiler:
|
||||||
|
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -472,7 +504,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
|||||||
|
|
||||||
public enum ChatControllerSubject: Equatable {
|
public enum ChatControllerSubject: Equatable {
|
||||||
public enum MessageSubject: Equatable {
|
public enum MessageSubject: Equatable {
|
||||||
case id(MessageId)
|
case id(EngineMessage.Id)
|
||||||
case timestamp(Int32)
|
case timestamp(Int32)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +649,7 @@ public final class PeerInfoNavigationSourceTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public protocol PeerInfoScreen: ViewController {
|
public protocol PeerInfoScreen: ViewController {
|
||||||
|
var peerId: PeerId { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ChatController: ViewController {
|
public protocol ChatController: ViewController {
|
||||||
@ -652,20 +684,21 @@ public enum FileMediaResourcePlaybackStatus: Equatable {
|
|||||||
|
|
||||||
public struct FileMediaResourceStatus: Equatable {
|
public struct FileMediaResourceStatus: Equatable {
|
||||||
public var mediaStatus: FileMediaResourceMediaStatus
|
public var mediaStatus: FileMediaResourceMediaStatus
|
||||||
public var fetchStatus: MediaResourceStatus
|
public var fetchStatus: EngineMediaResource.FetchStatus
|
||||||
|
|
||||||
public init(mediaStatus: FileMediaResourceMediaStatus, fetchStatus: MediaResourceStatus) {
|
public init(mediaStatus: FileMediaResourceMediaStatus, fetchStatus: EngineMediaResource.FetchStatus) {
|
||||||
self.mediaStatus = mediaStatus
|
self.mediaStatus = mediaStatus
|
||||||
self.fetchStatus = fetchStatus
|
self.fetchStatus = fetchStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FileMediaResourceMediaStatus: Equatable {
|
public enum FileMediaResourceMediaStatus: Equatable {
|
||||||
case fetchStatus(MediaResourceStatus)
|
case fetchStatus(EngineMediaResource.FetchStatus)
|
||||||
case playbackStatus(FileMediaResourcePlaybackStatus)
|
case playbackStatus(FileMediaResourcePlaybackStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ChatMessageItemNodeProtocol: ListViewItemNode {
|
public protocol ChatMessageItemNodeProtocol: ListViewItemNode {
|
||||||
func targetReactionView(value: MessageReaction.Reaction) -> UIView?
|
func targetReactionView(value: MessageReaction.Reaction) -> UIView?
|
||||||
|
func targetForStoryTransition(id: StoryId) -> UIView?
|
||||||
func contentFrame() -> CGRect
|
func contentFrame() -> CGRect
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public enum ChatListControllerLocation: Equatable {
|
public enum ChatListControllerLocation: Equatable {
|
||||||
case chatList(groupId: EngineChatList.Group)
|
case chatList(groupId: EngineChatList.Group)
|
||||||
case forum(peerId: PeerId)
|
case forum(peerId: EnginePeer.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ChatListController: ViewController {
|
public protocol ChatListController: ViewController {
|
||||||
@ -22,4 +21,6 @@ public protocol ChatListController: ViewController {
|
|||||||
func playSignUpCompletedAnimation()
|
func playSignUpCompletedAnimation()
|
||||||
|
|
||||||
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
|
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
|
||||||
|
|
||||||
|
func openStories(peerId: EnginePeer.Id)
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,12 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
public struct ChatListNodeAdditionalCategory {
|
public struct ChatListNodeAdditionalCategory {
|
||||||
public enum Appearance {
|
public enum Appearance: Equatable {
|
||||||
case option
|
case option(sectionTitle: String?)
|
||||||
case action
|
case action
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ public struct ChatListNodeAdditionalCategory {
|
|||||||
public var title: String
|
public var title: String
|
||||||
public var appearance: Appearance
|
public var appearance: Appearance
|
||||||
|
|
||||||
public init(id: Int, icon: UIImage?, smallIcon: UIImage?, title: String, appearance: Appearance = .option) {
|
public init(id: Int, icon: UIImage?, smallIcon: UIImage?, title: String, appearance: Appearance = .option(sectionTitle: nil)) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.smallIcon = smallIcon
|
self.smallIcon = smallIcon
|
||||||
@ -41,18 +40,20 @@ public enum ContactMultiselectionControllerMode {
|
|||||||
public struct ChatSelection {
|
public struct ChatSelection {
|
||||||
public var title: String
|
public var title: String
|
||||||
public var searchPlaceholder: String
|
public var searchPlaceholder: String
|
||||||
public var selectedChats: Set<PeerId>
|
public var selectedChats: Set<EnginePeer.Id>
|
||||||
public var additionalCategories: ContactMultiselectionControllerAdditionalCategories?
|
public var additionalCategories: ContactMultiselectionControllerAdditionalCategories?
|
||||||
public var chatListFilters: [ChatListFilter]?
|
public var chatListFilters: [ChatListFilter]?
|
||||||
public var displayAutoremoveTimeout: Bool
|
public var displayAutoremoveTimeout: Bool
|
||||||
|
public var displayPresence: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
searchPlaceholder: String,
|
searchPlaceholder: String,
|
||||||
selectedChats: Set<PeerId>,
|
selectedChats: Set<EnginePeer.Id>,
|
||||||
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
|
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
|
||||||
chatListFilters: [ChatListFilter]?,
|
chatListFilters: [ChatListFilter]?,
|
||||||
displayAutoremoveTimeout: Bool = false
|
displayAutoremoveTimeout: Bool = false,
|
||||||
|
displayPresence: Bool = false
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.searchPlaceholder = searchPlaceholder
|
self.searchPlaceholder = searchPlaceholder
|
||||||
@ -60,6 +61,7 @@ public enum ContactMultiselectionControllerMode {
|
|||||||
self.additionalCategories = additionalCategories
|
self.additionalCategories = additionalCategories
|
||||||
self.chatListFilters = chatListFilters
|
self.chatListFilters = chatListFilters
|
||||||
self.displayAutoremoveTimeout = displayAutoremoveTimeout
|
self.displayAutoremoveTimeout = displayAutoremoveTimeout
|
||||||
|
self.displayPresence = displayPresence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,8 +73,8 @@ public enum ContactMultiselectionControllerMode {
|
|||||||
|
|
||||||
public enum ContactListFilter {
|
public enum ContactListFilter {
|
||||||
case excludeSelf
|
case excludeSelf
|
||||||
case exclude([PeerId])
|
case exclude([EnginePeer.Id])
|
||||||
case disable([PeerId])
|
case disable([EnginePeer.Id])
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ContactMultiselectionControllerParams {
|
public final class ContactMultiselectionControllerParams {
|
||||||
|
@ -6,6 +6,7 @@ public protocol ContactSelectionController: ViewController {
|
|||||||
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { get }
|
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { get }
|
||||||
var displayProgress: Bool { get set }
|
var displayProgress: Bool { get set }
|
||||||
var dismissed: (() -> Void)? { get set }
|
var dismissed: (() -> Void)? { get set }
|
||||||
|
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void { get set }
|
||||||
|
|
||||||
func dismissSearch()
|
func dismissSearch()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Contacts
|
import Contacts
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public final class DeviceContactPhoneNumberData: Equatable {
|
public final class DeviceContactPhoneNumberData: Equatable {
|
||||||
@ -190,18 +189,18 @@ public let phonebookUsernamePathPrefix = "@id"
|
|||||||
private let phonebookUsernamePrefix = "https://t.me/" + phonebookUsernamePathPrefix
|
private let phonebookUsernamePrefix = "https://t.me/" + phonebookUsernamePathPrefix
|
||||||
|
|
||||||
public extension DeviceContactUrlData {
|
public extension DeviceContactUrlData {
|
||||||
convenience init(appProfile: PeerId) {
|
convenience init(appProfile: EnginePeer.Id) {
|
||||||
self.init(label: "Telegram", value: "\(phonebookUsernamePrefix)\(appProfile.id._internalGetInt64Value())")
|
self.init(label: "Telegram", value: "\(phonebookUsernamePrefix)\(appProfile.id._internalGetInt64Value())")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func parseAppSpecificContactReference(_ value: String) -> PeerId? {
|
public func parseAppSpecificContactReference(_ value: String) -> EnginePeer.Id? {
|
||||||
if !value.hasPrefix(phonebookUsernamePrefix) {
|
if !value.hasPrefix(phonebookUsernamePrefix) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...])
|
let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...])
|
||||||
if let id = Int64(idString) {
|
if let id = Int64(idString) {
|
||||||
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
|
return EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -466,8 +465,8 @@ public extension DeviceContactExtendedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public extension DeviceContactExtendedData {
|
public extension DeviceContactExtendedData {
|
||||||
convenience init?(peer: Peer) {
|
convenience init?(peer: EnginePeer) {
|
||||||
guard let user = peer as? TelegramUser else {
|
guard case let .user(user) = peer else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var phoneNumbers: [DeviceContactPhoneNumberData] = []
|
var phoneNumbers: [DeviceContactPhoneNumberData] = []
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
@ -12,10 +11,10 @@ public protocol DeviceContactDataManager: AnyObject {
|
|||||||
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError>
|
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError>
|
||||||
func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
||||||
func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>
|
func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>
|
||||||
func appSpecificReferences() -> Signal<[PeerId: DeviceContactBasicDataWithReference], NoError>
|
func appSpecificReferences() -> Signal<[EnginePeer.Id: DeviceContactBasicDataWithReference], NoError>
|
||||||
func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, PeerId?)], NoError>
|
func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, EnginePeer.Id?)], NoError>
|
||||||
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
||||||
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
|
||||||
func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError>
|
func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError>
|
||||||
func deleteContactWithAppSpecificReference(peerId: PeerId) -> Signal<Never, NoError>
|
func deleteContactWithAppSpecificReference(peerId: EnginePeer.Id) -> Signal<Never, NoError>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
@ -86,8 +86,15 @@ public struct FetchManagerPriorityKey: Comparable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FetchManagerLocation: Hashable {
|
public enum FetchManagerLocation: Hashable, CustomStringConvertible {
|
||||||
case chat(PeerId)
|
case chat(PeerId)
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case let .chat(peerId):
|
||||||
|
return "chat:\(peerId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FetchManagerForegroundDirection {
|
public enum FetchManagerForegroundDirection {
|
||||||
|
@ -18,7 +18,7 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for attribute in media.attributes {
|
for attribute in media.attributes {
|
||||||
if case let .Video(_, _, flags) = attribute {
|
if case let .Video(_, _, flags, _) = attribute {
|
||||||
if flags.contains(.supportsStreaming) {
|
if flags.contains(.supportsStreaming) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ public func isMediaStreamable(media: TelegramMediaFile) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for attribute in media.attributes {
|
for attribute in media.attributes {
|
||||||
if case let .Video(_, _, flags) = attribute {
|
if case let .Video(_, _, flags, _) = attribute {
|
||||||
if flags.contains(.supportsStreaming) {
|
if flags.contains(.supportsStreaming) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -95,8 +95,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? {
|
public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
|
||||||
func extractFileMedia(_ message: Message) -> TelegramMediaFile? {
|
func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
|
||||||
var file: TelegramMediaFile?
|
var file: TelegramMediaFile?
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let media = media as? TelegramMediaFile {
|
if let media = media as? TelegramMediaFile {
|
||||||
@ -120,7 +120,7 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
|
public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
|
||||||
if isGlobalSearch && !isDownloadList {
|
if isGlobalSearch && !isDownloadList {
|
||||||
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
|
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
|
||||||
} else if isRecentActions && !isDownloadList {
|
} else if isRecentActions && !isDownloadList {
|
||||||
|
@ -31,7 +31,7 @@ public final class OpenChatMessageParams {
|
|||||||
public let modal: Bool
|
public let modal: Bool
|
||||||
public let dismissInput: () -> Void
|
public let dismissInput: () -> Void
|
||||||
public let present: (ViewController, Any?) -> Void
|
public let present: (ViewController, Any?) -> Void
|
||||||
public let transitionNode: (MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
public let transitionNode: (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||||
public let addToTransitionSurface: (UIView) -> Void
|
public let addToTransitionSurface: (UIView) -> Void
|
||||||
public let openUrl: (String) -> Void
|
public let openUrl: (String) -> Void
|
||||||
public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void
|
public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void
|
||||||
@ -60,7 +60,7 @@ public final class OpenChatMessageParams {
|
|||||||
modal: Bool = false,
|
modal: Bool = false,
|
||||||
dismissInput: @escaping () -> Void,
|
dismissInput: @escaping () -> Void,
|
||||||
present: @escaping (ViewController, Any?) -> Void,
|
present: @escaping (ViewController, Any?) -> Void,
|
||||||
transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?,
|
transitionNode: @escaping (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?,
|
||||||
addToTransitionSurface: @escaping (UIView) -> Void,
|
addToTransitionSurface: @escaping (UIView) -> Void,
|
||||||
openUrl: @escaping (String) -> Void,
|
openUrl: @escaping (String) -> Void,
|
||||||
openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void,
|
openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ public final class PeerSelectionControllerParams {
|
|||||||
public let hasContactSelector: Bool
|
public let hasContactSelector: Bool
|
||||||
public let hasGlobalSearch: Bool
|
public let hasGlobalSearch: Bool
|
||||||
public let title: String?
|
public let title: String?
|
||||||
public let attemptSelection: ((Peer, Int64?) -> Void)?
|
public let attemptSelection: ((EnginePeer, Int64?) -> Void)?
|
||||||
public let createNewGroup: (() -> Void)?
|
public let createNewGroup: (() -> Void)?
|
||||||
public let pretendPresentedInModal: Bool
|
public let pretendPresentedInModal: Bool
|
||||||
public let multipleSelection: Bool
|
public let multipleSelection: Bool
|
||||||
@ -57,7 +56,7 @@ public final class PeerSelectionControllerParams {
|
|||||||
public let selectForumThreads: Bool
|
public let selectForumThreads: Bool
|
||||||
public let hasCreation: Bool
|
public let hasCreation: Bool
|
||||||
|
|
||||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil, forumPeerId: EnginePeer.Id? = nil, hasFilters: Bool = false, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false, hasCreation: Bool = false) {
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil, forumPeerId: EnginePeer.Id? = nil, hasFilters: Bool = false, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((EnginePeer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false, hasCreation: Bool = false) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.updatedPresentationData = updatedPresentationData
|
self.updatedPresentationData = updatedPresentationData
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
@ -87,8 +86,8 @@ public enum AttachmentTextInputPanelSendMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public protocol PeerSelectionController: ViewController {
|
public protocol PeerSelectionController: ViewController {
|
||||||
var peerSelected: ((Peer, Int64?) -> Void)? { get set }
|
var peerSelected: ((EnginePeer, Int64?) -> Void)? { get set }
|
||||||
var multiplePeersSelected: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? { get set }
|
var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? { get set }
|
||||||
var inProgress: Bool { get set }
|
var inProgress: Bool { get set }
|
||||||
var customDismiss: (() -> Void)? { get set }
|
var customDismiss: (() -> Void)? { get set }
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramAudio
|
import TelegramAudio
|
||||||
|
|
||||||
public enum RequestCallResult {
|
public enum RequestCallResult {
|
||||||
case requested
|
case requested
|
||||||
case alreadyInProgress(PeerId?)
|
case alreadyInProgress(EnginePeer.Id?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum JoinGroupCallManagerResult {
|
public enum JoinGroupCallManagerResult {
|
||||||
case joined
|
case joined
|
||||||
case alreadyInProgress(PeerId?)
|
case alreadyInProgress(EnginePeer.Id?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RequestScheduleGroupCallResult {
|
public enum RequestScheduleGroupCallResult {
|
||||||
case success
|
case success
|
||||||
case alreadyInProgress(PeerId?)
|
case alreadyInProgress(EnginePeer.Id?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct CallAuxiliaryServer {
|
public struct CallAuxiliaryServer {
|
||||||
@ -135,11 +134,11 @@ public protocol PresentationCall: AnyObject {
|
|||||||
var context: AccountContext { get }
|
var context: AccountContext { get }
|
||||||
var isIntegratedWithCallKit: Bool { get }
|
var isIntegratedWithCallKit: Bool { get }
|
||||||
var internalId: CallSessionInternalId { get }
|
var internalId: CallSessionInternalId { get }
|
||||||
var peerId: PeerId { get }
|
var peerId: EnginePeer.Id { get }
|
||||||
var isOutgoing: Bool { get }
|
var isOutgoing: Bool { get }
|
||||||
var isVideo: Bool { get }
|
var isVideo: Bool { get }
|
||||||
var isVideoPossible: Bool { get }
|
var isVideoPossible: Bool { get }
|
||||||
var peer: Peer? { get }
|
var peer: EnginePeer? { get }
|
||||||
|
|
||||||
var state: Signal<PresentationCallState, NoError> { get }
|
var state: Signal<PresentationCallState, NoError> { get }
|
||||||
var audioLevel: Signal<Float, NoError> { get }
|
var audioLevel: Signal<Float, NoError> { get }
|
||||||
@ -199,10 +198,10 @@ public struct PresentationGroupCallState: Equatable {
|
|||||||
case muted
|
case muted
|
||||||
}
|
}
|
||||||
|
|
||||||
public var myPeerId: PeerId
|
public var myPeerId: EnginePeer.Id
|
||||||
public var networkState: NetworkState
|
public var networkState: NetworkState
|
||||||
public var canManageCall: Bool
|
public var canManageCall: Bool
|
||||||
public var adminIds: Set<PeerId>
|
public var adminIds: Set<EnginePeer.Id>
|
||||||
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||||
public var defaultParticipantMuteState: DefaultParticipantMuteState?
|
public var defaultParticipantMuteState: DefaultParticipantMuteState?
|
||||||
public var recordingStartTimestamp: Int32?
|
public var recordingStartTimestamp: Int32?
|
||||||
@ -214,10 +213,10 @@ public struct PresentationGroupCallState: Equatable {
|
|||||||
public var isVideoWatchersLimitReached: Bool
|
public var isVideoWatchersLimitReached: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
myPeerId: PeerId,
|
myPeerId: EnginePeer.Id,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
canManageCall: Bool,
|
canManageCall: Bool,
|
||||||
adminIds: Set<PeerId>,
|
adminIds: Set<EnginePeer.Id>,
|
||||||
muteState: GroupCallParticipantsContext.Participant.MuteState?,
|
muteState: GroupCallParticipantsContext.Participant.MuteState?,
|
||||||
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
||||||
recordingStartTimestamp: Int32?,
|
recordingStartTimestamp: Int32?,
|
||||||
@ -249,14 +248,14 @@ public struct PresentationGroupCallSummaryState: Equatable {
|
|||||||
public var participantCount: Int
|
public var participantCount: Int
|
||||||
public var callState: PresentationGroupCallState
|
public var callState: PresentationGroupCallState
|
||||||
public var topParticipants: [GroupCallParticipantsContext.Participant]
|
public var topParticipants: [GroupCallParticipantsContext.Participant]
|
||||||
public var activeSpeakers: Set<PeerId>
|
public var activeSpeakers: Set<EnginePeer.Id>
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
info: GroupCallInfo?,
|
info: GroupCallInfo?,
|
||||||
participantCount: Int,
|
participantCount: Int,
|
||||||
callState: PresentationGroupCallState,
|
callState: PresentationGroupCallState,
|
||||||
topParticipants: [GroupCallParticipantsContext.Participant],
|
topParticipants: [GroupCallParticipantsContext.Participant],
|
||||||
activeSpeakers: Set<PeerId>
|
activeSpeakers: Set<EnginePeer.Id>
|
||||||
) {
|
) {
|
||||||
self.info = info
|
self.info = info
|
||||||
self.participantCount = participantCount
|
self.participantCount = participantCount
|
||||||
@ -298,13 +297,13 @@ public enum PresentationGroupCallMuteAction: Equatable {
|
|||||||
|
|
||||||
public struct PresentationGroupCallMembers: Equatable {
|
public struct PresentationGroupCallMembers: Equatable {
|
||||||
public var participants: [GroupCallParticipantsContext.Participant]
|
public var participants: [GroupCallParticipantsContext.Participant]
|
||||||
public var speakingParticipants: Set<PeerId>
|
public var speakingParticipants: Set<EnginePeer.Id>
|
||||||
public var totalCount: Int
|
public var totalCount: Int
|
||||||
public var loadMoreToken: String?
|
public var loadMoreToken: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
participants: [GroupCallParticipantsContext.Participant],
|
participants: [GroupCallParticipantsContext.Participant],
|
||||||
speakingParticipants: Set<PeerId>,
|
speakingParticipants: Set<EnginePeer.Id>,
|
||||||
totalCount: Int,
|
totalCount: Int,
|
||||||
loadMoreToken: String?
|
loadMoreToken: String?
|
||||||
) {
|
) {
|
||||||
@ -316,13 +315,13 @@ public struct PresentationGroupCallMembers: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class PresentationGroupCallMemberEvent {
|
public final class PresentationGroupCallMemberEvent {
|
||||||
public let peer: Peer
|
public let peer: EnginePeer
|
||||||
public let isContact: Bool
|
public let isContact: Bool
|
||||||
public let isInChatList: Bool
|
public let isInChatList: Bool
|
||||||
public let canUnmute: Bool
|
public let canUnmute: Bool
|
||||||
public let joined: Bool
|
public let joined: Bool
|
||||||
|
|
||||||
public init(peer: Peer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) {
|
public init(peer: EnginePeer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) {
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.isContact = isContact
|
self.isContact = isContact
|
||||||
self.isInChatList = isInChatList
|
self.isInChatList = isInChatList
|
||||||
@ -395,7 +394,7 @@ public protocol PresentationGroupCall: AnyObject {
|
|||||||
var account: Account { get }
|
var account: Account { get }
|
||||||
var accountContext: AccountContext { get }
|
var accountContext: AccountContext { get }
|
||||||
var internalId: CallSessionInternalId { get }
|
var internalId: CallSessionInternalId { get }
|
||||||
var peerId: PeerId { get }
|
var peerId: EnginePeer.Id { get }
|
||||||
|
|
||||||
var hasVideo: Bool { get }
|
var hasVideo: Bool { get }
|
||||||
var hasScreencast: Bool { get }
|
var hasScreencast: Bool { get }
|
||||||
@ -412,20 +411,20 @@ public protocol PresentationGroupCall: AnyObject {
|
|||||||
var stateVersion: Signal<Int, NoError> { get }
|
var stateVersion: Signal<Int, NoError> { get }
|
||||||
var summaryState: Signal<PresentationGroupCallSummaryState?, NoError> { get }
|
var summaryState: Signal<PresentationGroupCallSummaryState?, NoError> { get }
|
||||||
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
|
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
|
||||||
var audioLevels: Signal<[(PeerId, UInt32, Float, Bool)], NoError> { get }
|
var audioLevels: Signal<[(EnginePeer.Id, UInt32, Float, Bool)], NoError> { get }
|
||||||
var myAudioLevel: Signal<Float, NoError> { get }
|
var myAudioLevel: Signal<Float, NoError> { get }
|
||||||
var isMuted: Signal<Bool, NoError> { get }
|
var isMuted: Signal<Bool, NoError> { get }
|
||||||
var isNoiseSuppressionEnabled: Signal<Bool, NoError> { get }
|
var isNoiseSuppressionEnabled: Signal<Bool, NoError> { get }
|
||||||
|
|
||||||
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
|
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
|
||||||
var reconnectedAsEvents: Signal<Peer, NoError> { get }
|
var reconnectedAsEvents: Signal<EnginePeer, NoError> { get }
|
||||||
|
|
||||||
func toggleScheduledSubscription(_ subscribe: Bool)
|
func toggleScheduledSubscription(_ subscribe: Bool)
|
||||||
func schedule(timestamp: Int32)
|
func schedule(timestamp: Int32)
|
||||||
func startScheduled()
|
func startScheduled()
|
||||||
|
|
||||||
func reconnect(with invite: String)
|
func reconnect(with invite: String)
|
||||||
func reconnect(as peerId: PeerId)
|
func reconnect(as peerId: EnginePeer.Id)
|
||||||
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
||||||
|
|
||||||
func toggleIsMuted()
|
func toggleIsMuted()
|
||||||
@ -438,20 +437,20 @@ public protocol PresentationGroupCall: AnyObject {
|
|||||||
func disableScreencast()
|
func disableScreencast()
|
||||||
func switchVideoCamera()
|
func switchVideoCamera()
|
||||||
func updateDefaultParticipantsAreMuted(isMuted: Bool)
|
func updateDefaultParticipantsAreMuted(isMuted: Bool)
|
||||||
func setVolume(peerId: PeerId, volume: Int32, sync: Bool)
|
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
|
||||||
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
|
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
|
||||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||||
|
|
||||||
func playTone(_ tone: PresentationGroupCallTone)
|
func playTone(_ tone: PresentationGroupCallTone)
|
||||||
|
|
||||||
func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
|
func updateMuteState(peerId: EnginePeer.Id, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
|
||||||
func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?)
|
func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?)
|
||||||
|
|
||||||
func updateTitle(_ title: String)
|
func updateTitle(_ title: String)
|
||||||
|
|
||||||
func invitePeer(_ peerId: PeerId) -> Bool
|
func invitePeer(_ peerId: EnginePeer.Id) -> Bool
|
||||||
func removedPeer(_ peerId: PeerId)
|
func removedPeer(_ peerId: EnginePeer.Id)
|
||||||
var invitedPeers: Signal<[PeerId], NoError> { get }
|
var invitedPeers: Signal<[EnginePeer.Id], NoError> { get }
|
||||||
|
|
||||||
var inviteLinks: Signal<GroupCallInviteLinks?, NoError> { get }
|
var inviteLinks: Signal<GroupCallInviteLinks?, NoError> { get }
|
||||||
|
|
||||||
@ -464,8 +463,9 @@ public protocol PresentationGroupCall: AnyObject {
|
|||||||
public protocol PresentationCallManager: AnyObject {
|
public protocol PresentationCallManager: AnyObject {
|
||||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||||
var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }
|
var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }
|
||||||
|
var hasActiveCall: Bool { get }
|
||||||
|
|
||||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||||
func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
|
||||||
func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
|
func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
@ -59,8 +58,8 @@ public struct SharedMediaPlaybackAlbumArt: Equatable {
|
|||||||
|
|
||||||
public enum SharedMediaPlaybackDisplayData: Equatable {
|
public enum SharedMediaPlaybackDisplayData: Equatable {
|
||||||
case music(title: String?, performer: String?, albumArt: SharedMediaPlaybackAlbumArt?, long: Bool, caption: NSAttributedString?)
|
case music(title: String?, performer: String?, albumArt: SharedMediaPlaybackAlbumArt?, long: Bool, caption: NSAttributedString?)
|
||||||
case voice(author: Peer?, peer: Peer?)
|
case voice(author: EnginePeer?, peer: EnginePeer?)
|
||||||
case instantVideo(author: Peer?, peer: Peer?, timestamp: Int32)
|
case instantVideo(author: EnginePeer?, peer: EnginePeer?, timestamp: Int32)
|
||||||
|
|
||||||
public static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> Bool {
|
public static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
@ -71,13 +70,13 @@ public enum SharedMediaPlaybackDisplayData: Equatable {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .voice(lhsAuthor, lhsPeer):
|
case let .voice(lhsAuthor, lhsPeer):
|
||||||
if case let .voice(rhsAuthor, rhsPeer) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer) {
|
if case let .voice(rhsAuthor, rhsPeer) = rhs, lhsAuthor == rhsAuthor, lhsPeer == rhsPeer {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .instantVideo(lhsAuthor, lhsPeer, lhsTimestamp):
|
case let .instantVideo(lhsAuthor, lhsPeer, lhsTimestamp):
|
||||||
if case let .instantVideo(rhsAuthor, rhsPeer, rhsTimestamp) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer), lhsTimestamp == rhsTimestamp {
|
if case let .instantVideo(rhsAuthor, rhsPeer, rhsTimestamp) = rhs, lhsAuthor == rhsAuthor, lhsPeer == rhsPeer, lhsTimestamp == rhsTimestamp {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -125,10 +124,10 @@ public func areSharedMediaPlaylistItemIdsEqual(_ lhs: SharedMediaPlaylistItemId?
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId {
|
public struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId {
|
||||||
public let messageId: MessageId
|
public let messageId: EngineMessage.Id
|
||||||
public let messageIndex: MessageIndex
|
public let messageIndex: EngineMessage.Index
|
||||||
|
|
||||||
public init(messageId: MessageId, messageIndex: MessageIndex) {
|
public init(messageId: EngineMessage.Id, messageIndex: EngineMessage.Index) {
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
self.messageIndex = messageIndex
|
self.messageIndex = messageIndex
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject {
|
|||||||
func setSoundEnabled(_ value: Bool)
|
func setSoundEnabled(_ value: Bool)
|
||||||
func seek(_ timestamp: Double)
|
func seek(_ timestamp: Double)
|
||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||||
|
func continueWithOverridingAmbientMode(isAmbient: Bool)
|
||||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
||||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||||
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
|
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
|
||||||
@ -37,7 +38,7 @@ public protocol UniversalVideoContentNode: AnyObject {
|
|||||||
public protocol UniversalVideoContent {
|
public protocol UniversalVideoContent {
|
||||||
var id: AnyHashable { get }
|
var id: AnyHashable { get }
|
||||||
var dimensions: CGSize { get }
|
var dimensions: CGSize { get }
|
||||||
var duration: Int32 { get }
|
var duration: Double { get }
|
||||||
|
|
||||||
func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
|
func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
|
||||||
|
|
||||||
@ -283,6 +284,14 @@ public final class UniversalVideoNode: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
|
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||||
|
if let contentNode = contentNode {
|
||||||
|
contentNode.continueWithOverridingAmbientMode(isAmbient: isAmbient)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
||||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||||
if let contentNode = contentNode {
|
if let contentNode = contentNode {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
import TelegramCore
|
||||||
|
|
||||||
public struct WatchRunningTasks: Equatable {
|
public struct WatchRunningTasks: Equatable {
|
||||||
public let running: Bool
|
public let running: Bool
|
||||||
@ -18,6 +18,6 @@ public struct WatchRunningTasks: Equatable {
|
|||||||
|
|
||||||
public protocol WatchManager: AnyObject {
|
public protocol WatchManager: AnyObject {
|
||||||
var watchAppInstalled: Signal<Bool, NoError> { get }
|
var watchAppInstalled: Signal<Bool, NoError> { get }
|
||||||
var navigateToMessageRequested: Signal<MessageId, NoError> { get }
|
var navigateToMessageRequested: Signal<EngineMessage.Id, NoError> { get }
|
||||||
var runningTasks: Signal<WatchRunningTasks?, NoError> { get }
|
var runningTasks: Signal<WatchRunningTasks?, NoError> { get }
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ swift_library(
|
|||||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||||
"//submodules/AccountContext:AccountContext",
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/Markdown",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import Markdown
|
||||||
|
|
||||||
public final class AdInfoScreen: ViewController {
|
public final class AdInfoScreen: ViewController {
|
||||||
private final class Node: ViewControllerTracingNode {
|
private final class Node: ViewControllerTracingNode {
|
||||||
@ -84,9 +85,16 @@ public final class AdInfoScreen: ViewController {
|
|||||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||||
}
|
}
|
||||||
|
|
||||||
var openUrl: (() -> Void)?
|
var openUrl: ((String) -> Void)?
|
||||||
|
|
||||||
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_Text
|
#if DEBUG && false
|
||||||
|
let rawText = "First Line\n**Bold Text** [Description](http://google.com) text\n[url]\nabcdee"
|
||||||
|
#else
|
||||||
|
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_MarkdownText
|
||||||
|
#endif
|
||||||
|
|
||||||
|
let defaultUrl = self.presentationData.strings.SponsoredMessageInfo_Url
|
||||||
|
|
||||||
var items: [Item] = []
|
var items: [Item] = []
|
||||||
var didAddUrl = false
|
var didAddUrl = false
|
||||||
for component in rawText.components(separatedBy: "[url]") {
|
for component in rawText.components(separatedBy: "[url]") {
|
||||||
@ -100,20 +108,40 @@ public final class AdInfoScreen: ViewController {
|
|||||||
|
|
||||||
let textNode = ImmediateTextNode()
|
let textNode = ImmediateTextNode()
|
||||||
textNode.maximumNumberOfLines = 0
|
textNode.maximumNumberOfLines = 0
|
||||||
textNode.attributedText = NSAttributedString(string: itemText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
textNode.attributedText = parseMarkdownIntoAttributedString(itemText, attributes: MarkdownAttributes(
|
||||||
|
body: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
|
||||||
|
bold: MarkdownAttributeSet(font: Font.semibold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
|
||||||
|
link: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor),
|
||||||
|
linkAttribute: { url in
|
||||||
|
return ("URL", url)
|
||||||
|
}
|
||||||
|
))
|
||||||
items.append(.text(textNode))
|
items.append(.text(textNode))
|
||||||
|
textNode.highlightAttributeAction = { attributes in
|
||||||
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||||
|
return NSAttributedString.Key(rawValue: "URL")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textNode.tapAttributeAction = { attributes, _ in
|
||||||
|
if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
|
||||||
|
openUrl?(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)
|
||||||
|
|
||||||
if !didAddUrl {
|
if !didAddUrl {
|
||||||
didAddUrl = true
|
didAddUrl = true
|
||||||
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
||||||
openUrl?()
|
openUrl?(defaultUrl)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !didAddUrl {
|
if !didAddUrl {
|
||||||
didAddUrl = true
|
didAddUrl = true
|
||||||
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
||||||
openUrl?()
|
openUrl?(defaultUrl)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
self.items = items
|
self.items = items
|
||||||
@ -133,11 +161,11 @@ public final class AdInfoScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openUrl = { [weak self] in
|
openUrl = { [weak self] url in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_Url)
|
strongSelf.context.sharedContext.applicationBindings.openUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,10 +277,14 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
|||||||
guard let itemNode = self.contentNodes.removeValue(forKey: key) else {
|
guard let itemNode = self.contentNodes.removeValue(forKey: key) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
|
if animated {
|
||||||
itemNode?.removeFromSupernode()
|
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
|
||||||
})
|
itemNode?.removeFromSupernode()
|
||||||
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
})
|
||||||
|
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
} else {
|
||||||
|
itemNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CGSize(width: contentWidth, height: contentHeight)
|
return CGSize(width: contentWidth, height: contentHeight)
|
||||||
|
@ -277,7 +277,7 @@ public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width:
|
|||||||
return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
|
return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
public final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let path: String
|
private let path: String
|
||||||
private let width: Int
|
private let width: Int
|
||||||
@ -285,13 +285,13 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
|||||||
private let cache: VideoStickerFrameSourceCache?
|
private let cache: VideoStickerFrameSourceCache?
|
||||||
private let image: UIImage?
|
private let image: UIImage?
|
||||||
private let bytesPerRow: Int
|
private let bytesPerRow: Int
|
||||||
var frameCount: Int
|
public var frameCount: Int
|
||||||
let frameRate: Int
|
public let frameRate: Int
|
||||||
fileprivate var currentFrame: Int
|
fileprivate var currentFrame: Int
|
||||||
|
|
||||||
private let source: SoftwareVideoSource?
|
private let source: SoftwareVideoSource?
|
||||||
|
|
||||||
var frameIndex: Int {
|
public var frameIndex: Int {
|
||||||
if self.frameCount == 0 {
|
if self.frameCount == 0 {
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
@ -299,7 +299,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
|
public init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.path = path
|
self.path = path
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -334,7 +334,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
|||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
}
|
}
|
||||||
|
|
||||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||||
let frameIndex: Int
|
let frameIndex: Int
|
||||||
if self.frameCount > 0 {
|
if self.frameCount > 0 {
|
||||||
frameIndex = self.currentFrame % self.frameCount
|
frameIndex = self.currentFrame % self.frameCount
|
||||||
@ -415,11 +415,11 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipToEnd() {
|
public func skipToEnd() {
|
||||||
self.currentFrame = self.frameCount - 1
|
self.currentFrame = self.frameCount - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipToFrameIndex(_ index: Int) {
|
public func skipToFrameIndex(_ index: Int) {
|
||||||
self.currentFrame = index
|
self.currentFrame = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2998,7 +2998,7 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) {
|
|||||||
if ([self _implementsDisplay]) {
|
if ([self _implementsDisplay]) {
|
||||||
if (nowDisplay) {
|
if (nowDisplay) {
|
||||||
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
|
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
|
||||||
} else {
|
} else if (!self.disableClearContentsOnHide) {
|
||||||
[[self asyncLayer] cancelAsyncDisplay];
|
[[self asyncLayer] cancelAsyncDisplay];
|
||||||
//schedule clear contents on next runloop
|
//schedule clear contents on next runloop
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
@ -567,6 +567,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
|
|||||||
*/
|
*/
|
||||||
@property BOOL automaticallyRelayoutOnLayoutMarginsChanges;
|
@property BOOL automaticallyRelayoutOnLayoutMarginsChanges;
|
||||||
|
|
||||||
|
@property (nonatomic) bool disableClearContentsOnHide;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -80,6 +80,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
})
|
})
|
||||||
self.container.clipsToBounds = true
|
self.container.clipsToBounds = true
|
||||||
self.container.overflowInset = overflowInset
|
self.container.overflowInset = overflowInset
|
||||||
|
self.container.shouldAnimateDisappearance = true
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -539,7 +540,15 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
controller.setIgnoreAppearanceMethodInvocations(false)
|
controller.setIgnoreAppearanceMethodInvocations(false)
|
||||||
controller.viewDidDisappear(transition.isAnimated)
|
controller.viewDidDisappear(transition.isAnimated)
|
||||||
}
|
}
|
||||||
|
if let (layout, _, coveredByModalTransition) = self.validLayout {
|
||||||
|
self.update(layout: layout, controllers: [], coveredByModalTransition: coveredByModalTransition, transition: .immediate)
|
||||||
|
}
|
||||||
completion()
|
completion()
|
||||||
|
|
||||||
|
var bounds = self.bounds
|
||||||
|
bounds.origin.y = 0.0
|
||||||
|
self.bounds = bounds
|
||||||
|
|
||||||
return transition
|
return transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,7 @@ public protocol AttachmentContainable: ViewController {
|
|||||||
var cancelPanGesture: () -> Void { get set }
|
var cancelPanGesture: () -> Void { get set }
|
||||||
var isContainerPanning: () -> Bool { get set }
|
var isContainerPanning: () -> Bool { get set }
|
||||||
var isContainerExpanded: () -> Bool { get set }
|
var isContainerExpanded: () -> Bool { get set }
|
||||||
|
var mediaPickerContext: AttachmentMediaPickerContext? { get }
|
||||||
|
|
||||||
func isContainerPanningUpdated(_ panning: Bool)
|
func isContainerPanningUpdated(_ panning: Bool)
|
||||||
|
|
||||||
@ -206,7 +207,7 @@ public class AttachmentController: ViewController {
|
|||||||
private weak var controller: AttachmentController?
|
private weak var controller: AttachmentController?
|
||||||
private let dim: ASDisplayNode
|
private let dim: ASDisplayNode
|
||||||
private let shadowNode: ASImageNode
|
private let shadowNode: ASImageNode
|
||||||
private let container: AttachmentContainer
|
fileprivate let container: AttachmentContainer
|
||||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||||
let panel: AttachmentPanel
|
let panel: AttachmentPanel
|
||||||
|
|
||||||
@ -215,7 +216,7 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
private var modalProgress: CGFloat = 0.0
|
private var modalProgress: CGFloat = 0.0
|
||||||
private var isDismissing = false
|
fileprivate var isDismissing = false
|
||||||
|
|
||||||
private let captionDisposable = MetaDisposable()
|
private let captionDisposable = MetaDisposable()
|
||||||
private let mediaSelectionCountDisposable = MetaDisposable()
|
private let mediaSelectionCountDisposable = MetaDisposable()
|
||||||
@ -312,6 +313,10 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
self.container.updateModalProgress = { [weak self] progress, transition in
|
self.container.updateModalProgress = { [weak self] progress, transition in
|
||||||
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
|
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
|
||||||
|
var transition = transition
|
||||||
|
if strongSelf.container.supernode == nil {
|
||||||
|
transition = .animated(duration: 0.4, curve: .spring)
|
||||||
|
}
|
||||||
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
|
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
|
||||||
|
|
||||||
strongSelf.modalProgress = progress
|
strongSelf.modalProgress = progress
|
||||||
@ -644,7 +649,7 @@ public class AttachmentController: ViewController {
|
|||||||
} else {
|
} else {
|
||||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
||||||
|
|
||||||
let targetPosition = self.container.position
|
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||||
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height)
|
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height)
|
||||||
|
|
||||||
self.container.position = startPosition
|
self.container.position = startPosition
|
||||||
@ -673,6 +678,7 @@ public class AttachmentController: ViewController {
|
|||||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
|
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
|
||||||
self?.animating = false
|
self?.animating = false
|
||||||
|
self?.layer.removeAllAnimations()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
@ -740,12 +746,12 @@ public class AttachmentController: ViewController {
|
|||||||
let position: CGPoint
|
let position: CGPoint
|
||||||
let positionY = layout.size.height - size.height - insets.bottom - 40.0
|
let positionY = layout.size.height - size.height - insets.bottom - 40.0
|
||||||
if let sourceRect = controller.getSourceRect?() {
|
if let sourceRect = controller.getSourceRect?() {
|
||||||
position = CGPoint(x: floor(sourceRect.midX - size.width / 2.0), y: min(positionY, sourceRect.minY - size.height))
|
position = CGPoint(x: min(layout.size.width - size.width - 28.0, floor(sourceRect.midX - size.width / 2.0)), y: min(positionY, sourceRect.minY - size.height))
|
||||||
} else {
|
} else {
|
||||||
position = CGPoint(x: masterWidth - 174.0, y: positionY)
|
position = CGPoint(x: masterWidth - 174.0, y: positionY)
|
||||||
}
|
}
|
||||||
|
|
||||||
if controller.isStandalone {
|
if controller.isStandalone && !controller.forceSourceRect {
|
||||||
var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0)
|
var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0)
|
||||||
if let inputHeight = layout.inputHeight, inputHeight > 88.0 {
|
if let inputHeight = layout.inputHeight, inputHeight > 88.0 {
|
||||||
containerY = layout.size.height - inputHeight - size.height - 80.0
|
containerY = layout.size.height - inputHeight - size.height - 80.0
|
||||||
@ -876,7 +882,7 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
|
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
|
||||||
|
|
||||||
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
|
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady && !self.isDismissing {
|
||||||
self.wrapperNode.addSubnode(self.container)
|
self.wrapperNode.addSubnode(self.container)
|
||||||
|
|
||||||
if fromMenu, let _ = controller.getInputContainerNode() {
|
if fromMenu, let _ = controller.getInputContainerNode() {
|
||||||
@ -928,6 +934,8 @@ public class AttachmentController: ViewController {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var forceSourceRect = false
|
||||||
|
|
||||||
fileprivate var isStandalone: Bool {
|
fileprivate var isStandalone: Bool {
|
||||||
return self.buttons.contains(.standalone)
|
return self.buttons.contains(.standalone)
|
||||||
}
|
}
|
||||||
@ -964,12 +972,17 @@ public class AttachmentController: ViewController {
|
|||||||
self?.didDismiss()
|
self?.didDismiss()
|
||||||
self?._dismiss()
|
self?._dismiss()
|
||||||
completion?()
|
completion?()
|
||||||
|
self?.dismissedFlag = false
|
||||||
|
self?.node.isDismissing = false
|
||||||
|
self?.node.container.removeFromSupernode()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.didDismiss()
|
self.didDismiss()
|
||||||
self._dismiss()
|
self._dismiss()
|
||||||
completion?()
|
completion?()
|
||||||
|
self.node.isDismissing = false
|
||||||
|
self.node.container.removeFromSupernode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,6 +221,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
case .phoneLimitExceeded:
|
case .phoneLimitExceeded:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
||||||
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
|
case .appOutdated:
|
||||||
|
text = strongSelf.presentationData.strings.Login_ErrorAppOutdated
|
||||||
|
let updateUrl = strongSelf.presentationData.strings.InviteText_URL
|
||||||
|
let sharedContext = strongSelf.sharedContext
|
||||||
|
actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||||
|
sharedContext.applicationBindings.openUrl(updateUrl)
|
||||||
|
}))
|
||||||
case .phoneBanned:
|
case .phoneBanned:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
||||||
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||||
@ -581,6 +588,8 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
if let strongSelf = self, let controller = controller {
|
if let strongSelf = self, let controller = controller {
|
||||||
controller.inProgress = false
|
controller.inProgress = false
|
||||||
|
|
||||||
|
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
switch error {
|
switch error {
|
||||||
case .limitExceeded:
|
case .limitExceeded:
|
||||||
@ -589,6 +598,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
text = strongSelf.presentationData.strings.Login_InvalidPhoneError
|
text = strongSelf.presentationData.strings.Login_InvalidPhoneError
|
||||||
case .phoneLimitExceeded:
|
case .phoneLimitExceeded:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
||||||
|
case .appOutdated:
|
||||||
|
text = strongSelf.presentationData.strings.Login_ErrorAppOutdated
|
||||||
|
let updateUrl = strongSelf.presentationData.strings.InviteText_URL
|
||||||
|
let sharedContext = strongSelf.sharedContext
|
||||||
|
actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||||
|
sharedContext.applicationBindings.openUrl(updateUrl)
|
||||||
|
})]
|
||||||
case .phoneBanned:
|
case .phoneBanned:
|
||||||
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
text = strongSelf.presentationData.strings.Login_PhoneBannedError
|
||||||
case .generic:
|
case .generic:
|
||||||
@ -597,7 +613,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
|
|||||||
text = strongSelf.presentationData.strings.Login_NetworkError
|
text = strongSelf.presentationData.strings.Login_NetworkError
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -128,8 +128,9 @@ private final class PhoneAndCountryNode: ASDisplayNode {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let _ = strongSelf.processNumberChange(number: strongSelf.phoneInputNode.number)
|
let _ = strongSelf.processNumberChange(number: strongSelf.phoneInputNode.number)
|
||||||
|
|
||||||
if strongSelf.hasCountry {
|
let isServiceNumber = strongSelf.phoneInputNode.number.hasPrefix("+999")
|
||||||
strongSelf.hasNumberUpdated?(!strongSelf.phoneInputNode.codeAndNumber.2.isEmpty)
|
if strongSelf.hasCountry || isServiceNumber {
|
||||||
|
strongSelf.hasNumberUpdated?(!strongSelf.phoneInputNode.codeAndNumber.2.isEmpty || isServiceNumber)
|
||||||
} else {
|
} else {
|
||||||
strongSelf.hasNumberUpdated?(false)
|
strongSelf.hasNumberUpdated?(false)
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
|
|||||||
|
|
||||||
self.addPhotoButton.addTarget(self, action: #selector(self.addPhotoPressed), forControlEvents: .touchUpInside)
|
self.addPhotoButton.addTarget(self, action: #selector(self.addPhotoPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.termsNode.linkHighlightColor = self.theme.list.itemAccentColor.withAlphaComponent(0.5)
|
self.termsNode.linkHighlightColor = self.theme.list.itemAccentColor.withAlphaComponent(0.2)
|
||||||
self.termsNode.highlightAttributeAction = { attributes in
|
self.termsNode.highlightAttributeAction = { attributes in
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||||
|
@ -87,7 +87,7 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
|
|||||||
case .flashCall, .missedCall:
|
case .flashCall, .missedCall:
|
||||||
return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
case .fragment:
|
case .fragment:
|
||||||
return (NSAttributedString(string: "Send code via fragment", font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
case .none:
|
case .none:
|
||||||
return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
|
|||||||
case .flashCall, .missedCall:
|
case .flashCall, .missedCall:
|
||||||
return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
case .fragment:
|
case .fragment:
|
||||||
return (NSAttributedString(string: "Send code via fragment", font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
case .none:
|
case .none:
|
||||||
return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ swift_library(
|
|||||||
"//submodules/Emoji:Emoji",
|
"//submodules/Emoji:Emoji",
|
||||||
"//submodules/TinyThumbnail:TinyThumbnail",
|
"//submodules/TinyThumbnail:TinyThumbnail",
|
||||||
"//submodules/FastBlur:FastBlur",
|
"//submodules/FastBlur:FastBlur",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ import Display
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
@ -207,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode {
|
|||||||
self.backgroundNode.image = nil
|
self.backgroundNode.image = nil
|
||||||
|
|
||||||
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
||||||
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
|
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)]))
|
||||||
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil)
|
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil)
|
||||||
if videoContent.id != self.videoContent?.id {
|
if videoContent.id != self.videoContent?.id {
|
||||||
self.videoNode?.removeFromSupernode()
|
self.videoNode?.removeFromSupernode()
|
||||||
|
@ -577,7 +577,7 @@ private final class RecurrentConfirmationNode: ASDisplayNode {
|
|||||||
|
|
||||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||||
|
|
||||||
self.textNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withAlphaComponent(0.3)
|
self.textNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)
|
||||||
|
|
||||||
let attributedText = parseMarkdownIntoAttributedString(
|
let attributedText = parseMarkdownIntoAttributedString(
|
||||||
presentationData.strings.Bot_AccepRecurrentInfo(botName).string,
|
presentationData.strings.Bot_AccepRecurrentInfo(botName).string,
|
||||||
|
@ -8,7 +8,6 @@ import TelegramPresentationData
|
|||||||
import ItemListUI
|
import ItemListUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import Postbox
|
|
||||||
|
|
||||||
class BotCheckoutHeaderItem: ListViewItem, ItemListItem {
|
class BotCheckoutHeaderItem: ListViewItem, ItemListItem {
|
||||||
let account: Account
|
let account: Account
|
||||||
@ -168,7 +167,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
var imageApply: (() -> Void)?
|
var imageApply: (() -> Void)?
|
||||||
var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||||
var updatedFetchSignal: Signal<FetchResourceSourceType, FetchResourceError>?
|
var updatedFetchSignal: Signal<Never, NoError>?
|
||||||
if let photo = item.invoice.photo, let dimensions = photo.dimensions {
|
if let photo = item.invoice.photo, let dimensions = photo.dimensions {
|
||||||
let arguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: dimensions.cgSize.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
let arguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: dimensions.cgSize.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||||
imageApply = makeImageLayout(arguments)
|
imageApply = makeImageLayout(arguments)
|
||||||
@ -184,6 +183,10 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: .standalone(resource: photo.resource))
|
updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: .standalone(resource: photo.resource))
|
||||||
|
|> ignoreValues
|
||||||
|
|> `catch` { _ -> Signal<Never, NoError> in
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +194,9 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
let (botNameLayout, botNameApply) = makeBotNameLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.botName, font: textFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (botNameLayout, botNameApply) = makeBotNameLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.botName, font: textFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invoice.description, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: maxTextHeight - titleLayout.size.height - titleTextSpacing - botNameLayout.size.height - textBotNameSpacing), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let textLayoutMaxHeight: CGFloat = maxTextHeight - titleLayout.size.height - titleTextSpacing - botNameLayout.size.height - textBotNameSpacing
|
||||||
|
let textArguments = TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invoice.description, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: textLayoutMaxHeight), alignment: .natural, cutout: nil, insets: UIEdgeInsets())
|
||||||
|
let (textLayout, textApply) = makeTextLayout(textArguments)
|
||||||
|
|
||||||
let contentHeight: CGFloat
|
let contentHeight: CGFloat
|
||||||
if let _ = imageApply {
|
if let _ = imageApply {
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -371,7 +370,7 @@ private final class DayComponent: Component {
|
|||||||
private var currentSelection: DaySelection?
|
private var currentSelection: DaySelection?
|
||||||
|
|
||||||
private(set) var timestamp: Int32?
|
private(set) var timestamp: Int32?
|
||||||
private(set) var index: MessageIndex?
|
private(set) var index: EngineMessage.Index?
|
||||||
private var isHighlightingEnabled: Bool = false
|
private var isHighlightingEnabled: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -983,12 +982,12 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
|
|
||||||
private weak var controller: CalendarMessageScreen?
|
private weak var controller: CalendarMessageScreen?
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: EnginePeer.Id
|
||||||
private let initialTimestamp: Int32
|
private let initialTimestamp: Int32
|
||||||
private let enableMessageRangeDeletion: Bool
|
private let enableMessageRangeDeletion: Bool
|
||||||
private let canNavigateToEmptyDays: Bool
|
private let canNavigateToEmptyDays: Bool
|
||||||
private let navigateToOffset: (Int, Int32) -> Void
|
private let navigateToOffset: (Int, Int32) -> Void
|
||||||
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var scrollView: Scroller
|
private var scrollView: Scroller
|
||||||
@ -1019,13 +1018,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
init(
|
init(
|
||||||
controller: CalendarMessageScreen,
|
controller: CalendarMessageScreen,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
peerId: PeerId,
|
peerId: EnginePeer.Id,
|
||||||
calendarSource: SparseMessageCalendar,
|
calendarSource: SparseMessageCalendar,
|
||||||
initialTimestamp: Int32,
|
initialTimestamp: Int32,
|
||||||
enableMessageRangeDeletion: Bool,
|
enableMessageRangeDeletion: Bool,
|
||||||
canNavigateToEmptyDays: Bool,
|
canNavigateToEmptyDays: Bool,
|
||||||
navigateToOffset: @escaping (Int, Int32) -> Void,
|
navigateToOffset: @escaping (Int, Int32) -> Void,
|
||||||
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
) {
|
) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1370,7 +1369,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
if self.selectionState?.dayRange == nil {
|
if self.selectionState?.dayRange == nil {
|
||||||
if let selectionToolbarNode = self.selectionToolbarNode {
|
if let selectionToolbarNode = self.selectionToolbarNode {
|
||||||
let toolbarFrame = selectionToolbarNode.view.convert(selectionToolbarNode.bounds, to: self.view)
|
let toolbarFrame = selectionToolbarNode.view.convert(selectionToolbarNode.bounds, to: self.view)
|
||||||
self.controller?.present(TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip, style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { point in
|
self.controller?.present(TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip), style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { _, _ in
|
||||||
return .dismiss(consume: false)
|
return .dismiss(consume: false)
|
||||||
}), in: .current)
|
}), in: .current)
|
||||||
}
|
}
|
||||||
@ -1783,9 +1782,9 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
guard let calendarState = self.calendarState else {
|
guard let calendarState = self.calendarState else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var messageMap: [Message] = []
|
var messageMap: [EngineMessage] = []
|
||||||
for (_, entry) in calendarState.messagesByDay {
|
for (_, entry) in calendarState.messagesByDay {
|
||||||
messageMap.append(entry.message)
|
messageMap.append(EngineMessage(entry.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
||||||
@ -1805,7 +1804,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
mediaLoop: for media in message.media {
|
mediaLoop: for media in message.media {
|
||||||
switch media {
|
switch media {
|
||||||
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
||||||
updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media))
|
updatedMedia[i]![day] = DayMedia(message: message, media: EngineMedia(media))
|
||||||
break mediaLoop
|
break mediaLoop
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -1830,13 +1829,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: EnginePeer.Id
|
||||||
private let calendarSource: SparseMessageCalendar
|
private let calendarSource: SparseMessageCalendar
|
||||||
private let initialTimestamp: Int32
|
private let initialTimestamp: Int32
|
||||||
private let enableMessageRangeDeletion: Bool
|
private let enableMessageRangeDeletion: Bool
|
||||||
private let canNavigateToEmptyDays: Bool
|
private let canNavigateToEmptyDays: Bool
|
||||||
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
|
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
|
||||||
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
|
|
||||||
@ -1844,13 +1843,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
peerId: PeerId,
|
peerId: EnginePeer.Id,
|
||||||
calendarSource: SparseMessageCalendar,
|
calendarSource: SparseMessageCalendar,
|
||||||
initialTimestamp: Int32,
|
initialTimestamp: Int32,
|
||||||
enableMessageRangeDeletion: Bool,
|
enableMessageRangeDeletion: Bool,
|
||||||
canNavigateToEmptyDays: Bool,
|
canNavigateToEmptyDays: Bool,
|
||||||
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
|
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
|
||||||
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
|
@ -1,4 +1,44 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||||
|
"apple_resource_bundle",
|
||||||
|
"apple_resource_group",
|
||||||
|
)
|
||||||
|
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||||
|
"plist_fragment",
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "CameraMetalResources",
|
||||||
|
srcs = glob([
|
||||||
|
"MetalResources/**/*.*",
|
||||||
|
]),
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
plist_fragment(
|
||||||
|
name = "CameraBundleInfoPlist",
|
||||||
|
extension = "plist",
|
||||||
|
template =
|
||||||
|
"""
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.telegram.Camera</string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Camera</string>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
apple_resource_bundle(
|
||||||
|
name = "CameraBundle",
|
||||||
|
infoplists = [
|
||||||
|
":CameraBundleInfoPlist",
|
||||||
|
],
|
||||||
|
resources = [
|
||||||
|
":CameraMetalResources",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
swift_library(
|
swift_library(
|
||||||
name = "Camera",
|
name = "Camera",
|
||||||
@ -9,10 +49,15 @@ swift_library(
|
|||||||
copts = [
|
copts = [
|
||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
|
data = [
|
||||||
|
":CameraBundle",
|
||||||
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
"//submodules/Display:Display",
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/ImageBlur:ImageBlur",
|
||||||
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
30
submodules/Camera/MetalResources/camera.metal
Normal file
30
submodules/Camera/MetalResources/camera.metal
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
// Vertex input/output structure for passing results from vertex shader to fragment shader
|
||||||
|
struct VertexIO
|
||||||
|
{
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textureCoord [[user(texturecoord)]];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vertex shader for a textured quad
|
||||||
|
vertex VertexIO vertexPassThrough(const device packed_float4 *pPosition [[ buffer(0) ]],
|
||||||
|
const device packed_float2 *pTexCoords [[ buffer(1) ]],
|
||||||
|
uint vid [[ vertex_id ]])
|
||||||
|
{
|
||||||
|
VertexIO outVertex;
|
||||||
|
|
||||||
|
outVertex.position = pPosition[vid];
|
||||||
|
outVertex.textureCoord = pTexCoords[vid];
|
||||||
|
|
||||||
|
return outVertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fragment shader for a textured quad
|
||||||
|
fragment half4 fragmentPassThrough(VertexIO inputFragment [[ stage_in ]],
|
||||||
|
texture2d<half> inputTexture [[ texture(0) ]],
|
||||||
|
sampler samplr [[ sampler(0) ]])
|
||||||
|
{
|
||||||
|
return inputTexture.sample(samplr, inputFragment.textureCoord);
|
||||||
|
}
|
@ -1,21 +1,123 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import CoreImage
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
final class CameraSession {
|
||||||
|
private let singleSession: AVCaptureSession?
|
||||||
|
private let multiSession: Any?
|
||||||
|
|
||||||
|
let hasMultiCam: Bool
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported {
|
||||||
|
self.multiSession = AVCaptureMultiCamSession()
|
||||||
|
self.singleSession = nil
|
||||||
|
self.hasMultiCam = true
|
||||||
|
} else {
|
||||||
|
self.singleSession = AVCaptureSession()
|
||||||
|
self.multiSession = nil
|
||||||
|
self.hasMultiCam = false
|
||||||
|
}
|
||||||
|
self.session.sessionPreset = .inputPriority
|
||||||
|
}
|
||||||
|
|
||||||
|
var session: AVCaptureSession {
|
||||||
|
if #available(iOS 13.0, *), let multiSession = self.multiSession as? AVCaptureMultiCamSession {
|
||||||
|
return multiSession
|
||||||
|
} else if let session = self.singleSession {
|
||||||
|
return session
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsDualCam: Bool {
|
||||||
|
return self.multiSession != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class CameraDeviceContext {
|
||||||
|
private weak var session: CameraSession?
|
||||||
|
private weak var previewView: CameraSimplePreviewView?
|
||||||
|
|
||||||
|
private let exclusive: Bool
|
||||||
|
private let additional: Bool
|
||||||
|
|
||||||
|
let device = CameraDevice()
|
||||||
|
let input = CameraInput()
|
||||||
|
let output: CameraOutput
|
||||||
|
|
||||||
|
init(session: CameraSession, exclusive: Bool, additional: Bool) {
|
||||||
|
self.session = session
|
||||||
|
self.exclusive = exclusive
|
||||||
|
self.additional = additional
|
||||||
|
self.output = CameraOutput(exclusive: exclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
|
||||||
|
guard let session = self.session else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.previewView = previewView
|
||||||
|
|
||||||
|
self.device.configure(for: session, position: position, dual: !exclusive || additional)
|
||||||
|
self.device.configureDeviceFormat(maxDimensions: self.preferredMaxDimensions, maxFramerate: self.preferredMaxFrameRate)
|
||||||
|
self.input.configure(for: session, device: self.device, audio: audio)
|
||||||
|
self.output.configure(for: session, device: self.device, input: self.input, previewView: previewView, audio: audio, photo: photo, metadata: metadata)
|
||||||
|
|
||||||
|
self.output.configureVideoStabilization()
|
||||||
|
|
||||||
|
self.device.resetZoom(neutral: self.exclusive || !self.additional)
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
guard let session = self.session else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.output.invalidate(for: session)
|
||||||
|
self.input.invalidate(for: session)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var preferredMaxDimensions: CMVideoDimensions {
|
||||||
|
if self.additional {
|
||||||
|
return CMVideoDimensions(width: 1920, height: 1440)
|
||||||
|
} else {
|
||||||
|
return CMVideoDimensions(width: 1920, height: 1080)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var preferredMaxFrameRate: Double {
|
||||||
|
if !self.exclusive {
|
||||||
|
return 30.0
|
||||||
|
}
|
||||||
|
switch DeviceModel.current {
|
||||||
|
case .iPhone14ProMax, .iPhone13ProMax:
|
||||||
|
return 60.0
|
||||||
|
default:
|
||||||
|
return 30.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class CameraContext {
|
private final class CameraContext {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let session = AVCaptureSession()
|
|
||||||
private let device: CameraDevice
|
private let session: CameraSession
|
||||||
private let input = CameraInput()
|
|
||||||
private let output = CameraOutput()
|
private var mainDeviceContext: CameraDeviceContext?
|
||||||
|
private var additionalDeviceContext: CameraDeviceContext?
|
||||||
|
|
||||||
|
private let cameraImageContext = CIContext()
|
||||||
|
|
||||||
private let initialConfiguration: Camera.Configuration
|
private let initialConfiguration: Camera.Configuration
|
||||||
private var invalidated = false
|
private var invalidated = false
|
||||||
|
|
||||||
private var previousSampleBuffer: CMSampleBuffer?
|
|
||||||
var processSampleBuffer: ((CMSampleBuffer) -> Void)?
|
|
||||||
|
|
||||||
private let detectedCodesPipe = ValuePipe<[CameraCode]>()
|
private let detectedCodesPipe = ValuePipe<[CameraCode]>()
|
||||||
|
fileprivate let modeChangePromise = ValuePromise<Camera.ModeChange>(.none)
|
||||||
|
|
||||||
var previewNode: CameraPreviewNode? {
|
var previewNode: CameraPreviewNode? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -23,87 +125,414 @@ private final class CameraContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(queue: Queue, configuration: Camera.Configuration) {
|
var previewView: CameraPreviewView?
|
||||||
self.queue = queue
|
|
||||||
self.initialConfiguration = configuration
|
var simplePreviewView: CameraSimplePreviewView?
|
||||||
|
var secondaryPreviewView: CameraSimplePreviewView?
|
||||||
self.device = CameraDevice()
|
|
||||||
self.device.configure(for: self.session, position: configuration.position)
|
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||||
|
private var lastAdditionalSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||||
self.session.beginConfiguration()
|
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, front: Bool) {
|
||||||
self.session.sessionPreset = configuration.preset
|
Queue.concurrentDefaultQueue().async {
|
||||||
self.input.configure(for: self.session, device: self.device, audio: configuration.audio)
|
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||||
self.output.configure(for: self.session)
|
let size = ciImage.extent.size
|
||||||
self.session.commitConfiguration()
|
if front {
|
||||||
|
var transform = CGAffineTransformMakeScale(1.0, -1.0)
|
||||||
self.output.processSampleBuffer = { [weak self] sampleBuffer, connection in
|
transform = CGAffineTransformTranslate(transform, 0.0, -size.height)
|
||||||
if let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Video {
|
ciImage = ciImage.transformed(by: transform)
|
||||||
self?.previousSampleBuffer = sampleBuffer
|
|
||||||
self?.previewNode?.enqueue(sampleBuffer)
|
|
||||||
}
|
}
|
||||||
|
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size))
|
||||||
self?.queue.async {
|
if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||||
self?.processSampleBuffer?(sampleBuffer)
|
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||||
|
if front {
|
||||||
|
CameraSimplePreviewView.saveLastFrontImage(uiImage)
|
||||||
|
} else {
|
||||||
|
CameraSimplePreviewView.saveLastBackImage(uiImage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output.processCodes = { [weak self] codes in
|
|
||||||
self?.detectedCodesPipe.putNext(codes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(queue: Queue, session: CameraSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?, secondaryPreviewView: CameraSimplePreviewView?) {
|
||||||
|
self.queue = queue
|
||||||
|
self.session = session
|
||||||
|
self.initialConfiguration = configuration
|
||||||
|
self.simplePreviewView = previewView
|
||||||
|
self.secondaryPreviewView = secondaryPreviewView
|
||||||
|
|
||||||
|
self.positionValue = configuration.position
|
||||||
|
self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
|
||||||
|
|
||||||
|
self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(self.sessionRuntimeError),
|
||||||
|
name: .AVCaptureSessionRuntimeError,
|
||||||
|
object: self.session.session
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isSessionRunning = false
|
||||||
func startCapture() {
|
func startCapture() {
|
||||||
guard !self.session.isRunning else {
|
guard !self.session.session.isRunning else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.session.session.startRunning()
|
||||||
self.session.startRunning()
|
self.isSessionRunning = self.session.session.isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopCapture(invalidate: Bool = false) {
|
func stopCapture(invalidate: Bool = false) {
|
||||||
if invalidate {
|
if invalidate {
|
||||||
self.session.beginConfiguration()
|
self.mainDeviceContext?.device.resetZoom()
|
||||||
self.input.invalidate(for: self.session)
|
|
||||||
self.output.invalidate(for: self.session)
|
self.configure {
|
||||||
self.session.commitConfiguration()
|
self.mainDeviceContext?.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.session.stopRunning()
|
self.session.session.stopRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
func focus(at point: CGPoint) {
|
func focus(at point: CGPoint, autoFocus: Bool) {
|
||||||
self.device.setFocusPoint(point, focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: true)
|
let focusMode: AVCaptureDevice.FocusMode
|
||||||
}
|
let exposureMode: AVCaptureDevice.ExposureMode
|
||||||
|
if autoFocus {
|
||||||
func setFPS(_ fps: Float64) {
|
focusMode = .continuousAutoFocus
|
||||||
self.device.fps = fps
|
exposureMode = .continuousAutoExposure
|
||||||
}
|
|
||||||
|
|
||||||
func togglePosition() {
|
|
||||||
self.session.beginConfiguration()
|
|
||||||
self.input.invalidate(for: self.session)
|
|
||||||
let targetPosition: Camera.Position
|
|
||||||
if case .back = self.device.position {
|
|
||||||
targetPosition = .front
|
|
||||||
} else {
|
} else {
|
||||||
targetPosition = .back
|
focusMode = .autoFocus
|
||||||
|
exposureMode = .autoExpose
|
||||||
}
|
}
|
||||||
self.device.configure(for: self.session, position: targetPosition)
|
self.mainDeviceContext?.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true)
|
||||||
self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio)
|
}
|
||||||
self.session.commitConfiguration()
|
|
||||||
|
func setFps(_ fps: Float64) {
|
||||||
|
self.mainDeviceContext?.device.fps = fps
|
||||||
|
}
|
||||||
|
|
||||||
|
private var modeChange: Camera.ModeChange = .none {
|
||||||
|
didSet {
|
||||||
|
if oldValue != self.modeChange {
|
||||||
|
self.modeChangePromise.set(self.modeChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _positionPromise: ValuePromise<Camera.Position>
|
||||||
|
var position: Signal<Camera.Position, NoError> {
|
||||||
|
return self._positionPromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var positionValue: Camera.Position = .back
|
||||||
|
func togglePosition() {
|
||||||
|
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isDualCameraEnabled == true {
|
||||||
|
let targetPosition: Camera.Position
|
||||||
|
if case .back = self.positionValue {
|
||||||
|
targetPosition = .front
|
||||||
|
} else {
|
||||||
|
targetPosition = .back
|
||||||
|
}
|
||||||
|
self.positionValue = targetPosition
|
||||||
|
self._positionPromise.set(targetPosition)
|
||||||
|
|
||||||
|
mainDeviceContext.output.markPositionChange(position: targetPosition)
|
||||||
|
} else {
|
||||||
|
self.configure {
|
||||||
|
self.mainDeviceContext?.invalidate()
|
||||||
|
|
||||||
|
let targetPosition: Camera.Position
|
||||||
|
if case .back = mainDeviceContext.device.position {
|
||||||
|
targetPosition = .front
|
||||||
|
} else {
|
||||||
|
targetPosition = .back
|
||||||
|
}
|
||||||
|
self.positionValue = targetPosition
|
||||||
|
self._positionPromise.set(targetPosition)
|
||||||
|
self.modeChange = .position
|
||||||
|
|
||||||
|
mainDeviceContext.configure(position: targetPosition, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||||
|
|
||||||
|
self.queue.after(0.5) {
|
||||||
|
self.modeChange = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setPosition(_ position: Camera.Position) {
|
||||||
|
self.configure {
|
||||||
|
self.mainDeviceContext?.invalidate()
|
||||||
|
|
||||||
|
self._positionPromise.set(position)
|
||||||
|
self.positionValue = position
|
||||||
|
self.modeChange = .position
|
||||||
|
|
||||||
|
self.mainDeviceContext?.configure(position: position, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||||
|
|
||||||
|
self.queue.after(0.5) {
|
||||||
|
self.modeChange = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDualCameraEnabled: Bool?
|
||||||
|
public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) {
|
||||||
|
guard enabled != self.isDualCameraEnabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isDualCameraEnabled = enabled
|
||||||
|
|
||||||
|
if change {
|
||||||
|
self.modeChange = .dualCamera
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
self.configure {
|
||||||
|
self.mainDeviceContext?.invalidate()
|
||||||
|
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: false)
|
||||||
|
self.mainDeviceContext?.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||||
|
|
||||||
|
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: true)
|
||||||
|
self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false)
|
||||||
|
}
|
||||||
|
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||||
|
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.previewNode?.enqueue(sampleBuffer)
|
||||||
|
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording {
|
||||||
|
var front = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||||
|
}
|
||||||
|
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||||
|
self.lastSnapshotTimestamp = timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||||
|
guard let self, let additionalDeviceContext = self.additionalDeviceContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
if timestamp > self.lastAdditionalSnapshotTimestamp + 2.5, !additionalDeviceContext.output.isRecording {
|
||||||
|
var front = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||||
|
}
|
||||||
|
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||||
|
self.lastAdditionalSnapshotTimestamp = timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.configure {
|
||||||
|
self.mainDeviceContext?.invalidate()
|
||||||
|
self.additionalDeviceContext?.invalidate()
|
||||||
|
self.additionalDeviceContext = nil
|
||||||
|
|
||||||
|
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false)
|
||||||
|
self.mainDeviceContext?.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||||
|
}
|
||||||
|
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||||
|
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.previewNode?.enqueue(sampleBuffer)
|
||||||
|
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording {
|
||||||
|
var front = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||||
|
}
|
||||||
|
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||||
|
self.lastSnapshotTimestamp = timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.mainDeviceContext?.output.processCodes = { [weak self] codes in
|
||||||
|
self?.detectedCodesPipe.putNext(codes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if change {
|
||||||
|
if #available(iOS 13.0, *), let previewView = self.simplePreviewView {
|
||||||
|
if enabled, let secondaryPreviewView = self.secondaryPreviewView {
|
||||||
|
let _ = (combineLatest(previewView.isPreviewing, secondaryPreviewView.isPreviewing)
|
||||||
|
|> map { first, second in
|
||||||
|
return first && second
|
||||||
|
}
|
||||||
|
|> filter { $0 }
|
||||||
|
|> take(1)
|
||||||
|
|> delay(0.1, queue: self.queue)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||||
|
self?.modeChange = .none
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let _ = (previewView.isPreviewing
|
||||||
|
|> filter { $0 }
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||||
|
self?.modeChange = .none
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.queue.after(0.4) {
|
||||||
|
self.modeChange = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configure(_ f: () -> Void) {
|
||||||
|
self.session.session.beginConfiguration()
|
||||||
|
f()
|
||||||
|
self.session.session.commitConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasTorch: Signal<Bool, NoError> {
|
var hasTorch: Signal<Bool, NoError> {
|
||||||
return self.device.isFlashAvailable
|
return self.mainDeviceContext?.device.isTorchAvailable ?? .never()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTorchActive(_ active: Bool) {
|
func setTorchActive(_ active: Bool) {
|
||||||
self.device.setTorchActive(active)
|
self.mainDeviceContext?.device.setTorchActive(active)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFlashActive: Signal<Bool, NoError> {
|
||||||
|
return self.mainDeviceContext?.output.isFlashActive ?? .never()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _flashMode: Camera.FlashMode = .off {
|
||||||
|
didSet {
|
||||||
|
self._flashModePromise.set(self._flashMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var _flashModePromise = ValuePromise<Camera.FlashMode>(.off)
|
||||||
|
var flashMode: Signal<Camera.FlashMode, NoError> {
|
||||||
|
return self._flashModePromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFlashMode(_ mode: Camera.FlashMode) {
|
||||||
|
self._flashMode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func setZoomLevel(_ zoomLevel: CGFloat) {
|
||||||
|
self.mainDeviceContext?.device.setZoomLevel(zoomLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setZoomDelta(_ zoomDelta: CGFloat) {
|
||||||
|
self.mainDeviceContext?.device.setZoomDelta(zoomDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
|
||||||
|
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
|
||||||
|
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||||
|
let dualPosition = self.positionValue
|
||||||
|
return combineLatest(
|
||||||
|
mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode),
|
||||||
|
additionalDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode)
|
||||||
|
) |> map { main, additional in
|
||||||
|
if case let .finished(mainImage, _, _) = main, case let .finished(additionalImage, _, _) = additional {
|
||||||
|
if dualPosition == .front {
|
||||||
|
return .finished(additionalImage, mainImage, CACurrentMediaTime())
|
||||||
|
} else {
|
||||||
|
return .finished(mainImage, additionalImage, CACurrentMediaTime())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .began
|
||||||
|
}
|
||||||
|
} |> distinctUntilChanged
|
||||||
|
} else {
|
||||||
|
return mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startRecording() -> Signal<Double, NoError> {
|
||||||
|
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
mainDeviceContext.device.setTorchMode(self._flashMode)
|
||||||
|
|
||||||
|
let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
|
||||||
|
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||||
|
return combineLatest(
|
||||||
|
mainDeviceContext.output.startRecording(isDualCamera: true, position: self.positionValue, orientation: orientation),
|
||||||
|
additionalDeviceContext.output.startRecording(isDualCamera: true, orientation: .portrait)
|
||||||
|
) |> map { value, _ in
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mainDeviceContext.output.startRecording(isDualCamera: false, orientation: orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||||
|
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||||
|
return combineLatest(
|
||||||
|
mainDeviceContext.output.stopRecording(),
|
||||||
|
additionalDeviceContext.output.stopRecording()
|
||||||
|
) |> mapToSignal { main, additional in
|
||||||
|
if case let .finished(mainResult, _, duration, positionChangeTimestamps, _) = main, case let .finished(additionalResult, _, _, _, _) = additional {
|
||||||
|
var additionalTransitionImage = additionalResult.1
|
||||||
|
if let cgImage = additionalResult.1.cgImage {
|
||||||
|
additionalTransitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||||
|
}
|
||||||
|
return .single(.finished(mainResult, (additionalResult.0, additionalTransitionImage, true, additionalResult.3), duration, positionChangeTimestamps, CACurrentMediaTime()))
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mirror = self.positionValue == .front
|
||||||
|
return mainDeviceContext.output.stopRecording()
|
||||||
|
|> map { result -> VideoCaptureResult in
|
||||||
|
if case let .finished(mainResult, _, duration, positionChangeTimestamps, time) = result {
|
||||||
|
var transitionImage = mainResult.1
|
||||||
|
if mirror, let cgImage = transitionImage.cgImage {
|
||||||
|
transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||||
|
}
|
||||||
|
return .finished((mainResult.0, transitionImage, mirror, mainResult.3), nil, duration, positionChangeTimestamps, time)
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var detectedCodes: Signal<[CameraCode], NoError> {
|
var detectedCodes: Signal<[CameraCode], NoError> {
|
||||||
return self.detectedCodesPipe.signal()
|
return self.detectedCodesPipe.signal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func sessionInterruptionEnded(notification: NSNotification) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func sessionRuntimeError(notification: NSNotification) {
|
||||||
|
guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = AVError(_nsError: errorValue)
|
||||||
|
Logger.shared.log("Camera", "Runtime error: \(error)")
|
||||||
|
|
||||||
|
if error.code == .mediaServicesWereReset {
|
||||||
|
self.queue.async {
|
||||||
|
if self.isSessionRunning {
|
||||||
|
self.session.session.startRunning()
|
||||||
|
self.isSessionRunning = self.session.session.isRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class Camera {
|
public final class Camera {
|
||||||
@ -111,25 +540,51 @@ public final class Camera {
|
|||||||
public typealias Position = AVCaptureDevice.Position
|
public typealias Position = AVCaptureDevice.Position
|
||||||
public typealias FocusMode = AVCaptureDevice.FocusMode
|
public typealias FocusMode = AVCaptureDevice.FocusMode
|
||||||
public typealias ExposureMode = AVCaptureDevice.ExposureMode
|
public typealias ExposureMode = AVCaptureDevice.ExposureMode
|
||||||
|
public typealias FlashMode = AVCaptureDevice.FlashMode
|
||||||
|
|
||||||
public struct Configuration {
|
public struct Configuration {
|
||||||
let preset: Preset
|
let preset: Preset
|
||||||
let position: Position
|
let position: Position
|
||||||
|
let isDualEnabled: Bool
|
||||||
let audio: Bool
|
let audio: Bool
|
||||||
|
let photo: Bool
|
||||||
|
let metadata: Bool
|
||||||
|
let preferredFps: Double
|
||||||
|
|
||||||
public init(preset: Preset, position: Position, audio: Bool) {
|
public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double) {
|
||||||
self.preset = preset
|
self.preset = preset
|
||||||
self.position = position
|
self.position = position
|
||||||
|
self.isDualEnabled = isDualEnabled
|
||||||
self.audio = audio
|
self.audio = audio
|
||||||
|
self.photo = photo
|
||||||
|
self.metadata = metadata
|
||||||
|
self.preferredFps = preferredFps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let queue = Queue()
|
private let queue = Queue()
|
||||||
private var contextRef: Unmanaged<CameraContext>?
|
private var contextRef: Unmanaged<CameraContext>?
|
||||||
|
|
||||||
|
private weak var previewView: CameraPreviewView?
|
||||||
|
|
||||||
public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true)) {
|
public let metrics: Camera.Metrics
|
||||||
|
|
||||||
|
public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false, preferredFps: 60.0), previewView: CameraSimplePreviewView? = nil, secondaryPreviewView: CameraSimplePreviewView? = nil) {
|
||||||
|
self.metrics = Camera.Metrics(model: DeviceModel.current)
|
||||||
|
|
||||||
|
let session = CameraSession()
|
||||||
|
session.session.usesApplicationAudioSession = true
|
||||||
|
session.session.automaticallyConfiguresApplicationAudioSession = false
|
||||||
|
session.session.automaticallyConfiguresCaptureDeviceForWideColor = false
|
||||||
|
if let previewView {
|
||||||
|
previewView.setSession(session.session, autoConnect: !session.hasMultiCam)
|
||||||
|
}
|
||||||
|
if let secondaryPreviewView, session.hasMultiCam {
|
||||||
|
secondaryPreviewView.setSession(session.session, autoConnect: false)
|
||||||
|
}
|
||||||
|
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
let context = CameraContext(queue: self.queue, configuration: configuration)
|
let context = CameraContext(queue: self.queue, session: session, configuration: configuration, metrics: self.metrics, previewView: previewView, secondaryPreviewView: secondaryPreviewView)
|
||||||
self.contextRef = Unmanaged.passRetained(context)
|
self.contextRef = Unmanaged.passRetained(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,19 +597,41 @@ public final class Camera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func startCapture() {
|
public func startCapture() {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
#else
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.startCapture()
|
context.startCapture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stopCapture(invalidate: Bool = false) {
|
public func stopCapture(invalidate: Bool = false) {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
#else
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.stopCapture(invalidate: invalidate)
|
context.stopCapture(invalidate: invalidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public var position: Signal<Camera.Position, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.position.start(next: { flashMode in
|
||||||
|
subscriber.putNext(flashMode)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func togglePosition() {
|
public func togglePosition() {
|
||||||
@ -165,22 +642,107 @@ public final class Camera {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func takePhoto() -> Signal<Void, NoError> {
|
public func setPosition(_ position: Camera.Position) {
|
||||||
return .never()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func focus(at point: CGPoint) {
|
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.focus(at: point)
|
context.setPosition(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setFPS(_ fps: Double) {
|
public func setDualCameraEnabled(_ enabled: Bool) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.setFPS(fps)
|
context.setDualCameraEnabled(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.takePhoto().start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startRecording() -> Signal<Double, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.startRecording().start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.stopRecording().start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func focus(at point: CGPoint, autoFocus: Bool = true) {
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.focus(at: point, autoFocus: autoFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setFps(_ fps: Double) {
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.setFps(fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setFlashMode(_ flashMode: FlashMode) {
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.setFlashMode(flashMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setZoomLevel(_ zoomLevel: CGFloat) {
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.setZoomLevel(zoomLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func setZoomDelta(_ zoomDelta: CGFloat) {
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.setZoomDelta(zoomDelta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,6 +762,39 @@ public final class Camera {
|
|||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
disposable.set(context.hasTorch.start(next: { hasTorch in
|
disposable.set(context.hasTorch.start(next: { hasTorch in
|
||||||
subscriber.putNext(hasTorch)
|
subscriber.putNext(hasTorch)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isFlashActive: Signal<Bool, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.isFlashActive.start(next: { isFlashActive in
|
||||||
|
subscriber.putNext(isFlashActive)
|
||||||
|
}, completed: {
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var flashMode: Signal<Camera.FlashMode, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.flashMode.start(next: { flashMode in
|
||||||
|
subscriber.putNext(flashMode)
|
||||||
|
}, completed: {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -222,10 +817,31 @@ public final class Camera {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setProcessSampleBuffer(_ block: ((CMSampleBuffer) -> Void)?) {
|
public func attachPreviewView(_ view: CameraPreviewView) {
|
||||||
|
self.previewView = view
|
||||||
|
let viewRef: Unmanaged<CameraPreviewView> = Unmanaged.passRetained(view)
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
context.processSampleBuffer = block
|
context.previewView = viewRef.takeUnretainedValue()
|
||||||
|
viewRef.release()
|
||||||
|
} else {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
viewRef.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func attachSimplePreviewView(_ view: CameraSimplePreviewView) {
|
||||||
|
let viewRef: Unmanaged<CameraSimplePreviewView> = Unmanaged.passRetained(view)
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
context.simplePreviewView = viewRef.takeUnretainedValue()
|
||||||
|
viewRef.release()
|
||||||
|
} else {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
viewRef.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,4 +859,41 @@ public final class Camera {
|
|||||||
return disposable
|
return disposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ModeChange: Equatable {
|
||||||
|
case none
|
||||||
|
case position
|
||||||
|
case dualCamera
|
||||||
|
}
|
||||||
|
public var modeChange: Signal<ModeChange, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.queue.async {
|
||||||
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
disposable.set(context.modeChangePromise.get().start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var isDualCameraSupported: Bool {
|
||||||
|
if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported && !DeviceModel.current.isIpad {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class CameraHolder {
|
||||||
|
public let camera: Camera
|
||||||
|
public let previewView: CameraPreviewView
|
||||||
|
|
||||||
|
public init(camera: Camera, previewView: CameraPreviewView) {
|
||||||
|
self.camera = camera
|
||||||
|
self.previewView = previewView
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,139 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
private let defaultFPS: Double = 30.0
|
private let defaultFPS: Double = 30.0
|
||||||
|
|
||||||
final class CameraDevice {
|
final class CameraDevice {
|
||||||
public private(set) var videoDevice: AVCaptureDevice? = nil
|
|
||||||
public private(set) var audioDevice: AVCaptureDevice? = nil
|
|
||||||
private var videoDevicePromise = Promise<AVCaptureDevice>()
|
|
||||||
|
|
||||||
init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
var position: Camera.Position = .back
|
var position: Camera.Position = .back
|
||||||
|
|
||||||
func configure(for session: AVCaptureSession, position: Camera.Position) {
|
deinit {
|
||||||
self.position = position
|
|
||||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
|
||||||
self.videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first
|
|
||||||
} else {
|
|
||||||
self.videoDevice = AVCaptureDevice.devices(for: .video).filter { $0.position == position }.first
|
|
||||||
}
|
|
||||||
if let videoDevice = self.videoDevice {
|
if let videoDevice = self.videoDevice {
|
||||||
self.videoDevicePromise.set(.single(videoDevice))
|
self.unsubscribeFromChanges(videoDevice)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public private(set) var videoDevice: AVCaptureDevice? = nil {
|
||||||
|
didSet {
|
||||||
|
if let previousVideoDevice = oldValue {
|
||||||
|
self.unsubscribeFromChanges(previousVideoDevice)
|
||||||
|
}
|
||||||
|
self.videoDevicePromise.set(.single(self.videoDevice))
|
||||||
|
if let videoDevice = self.videoDevice {
|
||||||
|
self.subscribeForChanges(videoDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var videoDevicePromise = Promise<AVCaptureDevice?>()
|
||||||
|
|
||||||
|
public private(set) var audioDevice: AVCaptureDevice? = nil
|
||||||
|
|
||||||
|
func configure(for session: CameraSession, position: Camera.Position, dual: Bool) {
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
var selectedDevice: AVCaptureDevice?
|
||||||
|
if #available(iOS 13.0, *), position != .front && !dual {
|
||||||
|
if let device = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: position) {
|
||||||
|
selectedDevice = device
|
||||||
|
} else if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: position) {
|
||||||
|
selectedDevice = device
|
||||||
|
} else if let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: position) {
|
||||||
|
selectedDevice = device
|
||||||
|
} else if let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first {
|
||||||
|
selectedDevice = device
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if selectedDevice == nil {
|
||||||
|
selectedDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedDevice == nil, #available(iOS 13.0, *) {
|
||||||
|
let allDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInTripleCamera, .builtInTelephotoCamera, .builtInDualWideCamera, .builtInTrueDepthCamera, .builtInWideAngleCamera, .builtInUltraWideCamera], mediaType: .video, position: position).devices
|
||||||
|
Logger.shared.log("Camera", "No device selected, availabled devices: \(allDevices)")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoDevice = selectedDevice
|
||||||
|
self.videoDevicePromise.set(.single(selectedDevice))
|
||||||
|
|
||||||
self.audioDevice = AVCaptureDevice.default(for: .audio)
|
self.audioDevice = AVCaptureDevice.default(for: .audio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureDeviceFormat(maxDimensions: CMVideoDimensions, maxFramerate: Double) {
|
||||||
|
guard let device = self.videoDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transaction(device) { device in
|
||||||
|
var maxWidth: Int32 = 0
|
||||||
|
var maxHeight: Int32 = 0
|
||||||
|
var hasSecondaryZoomLevels = false
|
||||||
|
var candidates: [AVCaptureDevice.Format] = []
|
||||||
|
outer: for format in device.formats {
|
||||||
|
if format.mediaType != .video || format.value(forKey: "isPhotoFormat") as? Bool == true {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
|
||||||
|
if dimensions.width >= maxWidth && dimensions.width <= maxDimensions.width && dimensions.height >= maxHeight && dimensions.height <= maxDimensions.height {
|
||||||
|
if dimensions.width > maxWidth {
|
||||||
|
hasSecondaryZoomLevels = false
|
||||||
|
candidates.removeAll()
|
||||||
|
}
|
||||||
|
let subtype = CMFormatDescriptionGetMediaSubType(format.formatDescription)
|
||||||
|
if subtype == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
|
||||||
|
for range in format.videoSupportedFrameRateRanges {
|
||||||
|
if range.maxFrameRate > 60 {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxWidth = dimensions.width
|
||||||
|
maxHeight = dimensions.height
|
||||||
|
|
||||||
|
if #available(iOS 16.0, *), !format.secondaryNativeResolutionZoomFactors.isEmpty {
|
||||||
|
hasSecondaryZoomLevels = true
|
||||||
|
candidates.append(format)
|
||||||
|
} else if !hasSecondaryZoomLevels {
|
||||||
|
candidates.append(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !candidates.isEmpty {
|
||||||
|
var bestFormat: AVCaptureDevice.Format?
|
||||||
|
outer: for format in candidates {
|
||||||
|
for range in format.videoSupportedFrameRateRanges {
|
||||||
|
if range.maxFrameRate > maxFramerate {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
bestFormat = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bestFormat == nil {
|
||||||
|
bestFormat = candidates.last
|
||||||
|
}
|
||||||
|
device.activeFormat = bestFormat!
|
||||||
|
|
||||||
|
Logger.shared.log("Camera", "Selected format:")
|
||||||
|
Logger.shared.log("Camera", bestFormat!.description)
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "No format selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.shared.log("Camera", "Available formats:")
|
||||||
|
for format in device.formats {
|
||||||
|
Logger.shared.log("Camera", format.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let targetFPS = device.actualFPS(maxFramerate) {
|
||||||
|
device.activeVideoMinFrameDuration = targetFPS.duration
|
||||||
|
device.activeVideoMaxFrameDuration = targetFPS.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func transaction(_ device: AVCaptureDevice, update: (AVCaptureDevice) -> Void) {
|
func transaction(_ device: AVCaptureDevice, update: (AVCaptureDevice) -> Void) {
|
||||||
if let _ = try? device.lockForConfiguration() {
|
if let _ = try? device.lockForConfiguration() {
|
||||||
update(device)
|
update(device)
|
||||||
@ -34,16 +141,16 @@ final class CameraDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func subscribeForChanges() {
|
private func subscribeForChanges(_ device: AVCaptureDevice) {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice)
|
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func unsubscribeFromChanges() {
|
private func unsubscribeFromChanges(_ device: AVCaptureDevice) {
|
||||||
NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice)
|
NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func subjectAreaChanged() {
|
@objc private func subjectAreaChanged() {
|
||||||
|
self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fps: Double = defaultFPS {
|
var fps: Double = defaultFPS {
|
||||||
@ -61,26 +168,13 @@ final class CameraDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*var isFlashActive: Signal<Bool, NoError> {
|
var isTorchAvailable: Signal<Bool, NoError> {
|
||||||
return self.videoDevicePromise.get()
|
return self.videoDevicePromise.get()
|
||||||
|> mapToSignal { device -> Signal<Bool, NoError> in
|
|> mapToSignal { device -> Signal<Bool, NoError> in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
subscriber.putNext(device.isFlashActive)
|
guard let device else {
|
||||||
let observer = device.observe(\.isFlashActive, options: [.new], changeHandler: { device, _ in
|
return EmptyDisposable
|
||||||
subscriber.putNext(device.isFlashActive)
|
|
||||||
})
|
|
||||||
return ActionDisposable {
|
|
||||||
observer.invalidate()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|> distinctUntilChanged
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
var isFlashAvailable: Signal<Bool, NoError> {
|
|
||||||
return self.videoDevicePromise.get()
|
|
||||||
|> mapToSignal { device -> Signal<Bool, NoError> in
|
|
||||||
return Signal { subscriber in
|
|
||||||
subscriber.putNext(device.isFlashAvailable)
|
subscriber.putNext(device.isFlashAvailable)
|
||||||
let observer = device.observe(\.isFlashAvailable, options: [.new], changeHandler: { device, _ in
|
let observer = device.observe(\.isFlashAvailable, options: [.new], changeHandler: { device, _ in
|
||||||
subscriber.putNext(device.isFlashAvailable)
|
subscriber.putNext(device.isFlashAvailable)
|
||||||
@ -97,6 +191,9 @@ final class CameraDevice {
|
|||||||
return self.videoDevicePromise.get()
|
return self.videoDevicePromise.get()
|
||||||
|> mapToSignal { device -> Signal<Bool, NoError> in
|
|> mapToSignal { device -> Signal<Bool, NoError> in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
|
guard let device else {
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
subscriber.putNext(device.isAdjustingFocus)
|
subscriber.putNext(device.isAdjustingFocus)
|
||||||
let observer = device.observe(\.isAdjustingFocus, options: [.new], changeHandler: { device, _ in
|
let observer = device.observe(\.isAdjustingFocus, options: [.new], changeHandler: { device, _ in
|
||||||
subscriber.putNext(device.isAdjustingFocus)
|
subscriber.putNext(device.isAdjustingFocus)
|
||||||
@ -122,6 +219,12 @@ final class CameraDevice {
|
|||||||
device.focusPointOfInterest = point
|
device.focusPointOfInterest = point
|
||||||
device.focusMode = focusMode
|
device.focusMode = focusMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
|
||||||
|
|
||||||
|
if abs(device.exposureTargetBias) > 0.0 {
|
||||||
|
device.setExposureTargetBias(0.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,4 +247,53 @@ final class CameraDevice {
|
|||||||
device.torchMode = active ? .on : .off
|
device.torchMode = active ? .on : .off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setTorchMode(_ flashMode: AVCaptureDevice.FlashMode) {
|
||||||
|
guard let device = self.videoDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transaction(device) { device in
|
||||||
|
let torchMode: AVCaptureDevice.TorchMode
|
||||||
|
switch flashMode {
|
||||||
|
case .on:
|
||||||
|
torchMode = .on
|
||||||
|
case .off:
|
||||||
|
torchMode = .off
|
||||||
|
case .auto:
|
||||||
|
torchMode = .auto
|
||||||
|
@unknown default:
|
||||||
|
torchMode = .off
|
||||||
|
}
|
||||||
|
if device.isTorchModeSupported(torchMode) {
|
||||||
|
device.torchMode = torchMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setZoomLevel(_ zoomLevel: CGFloat) {
|
||||||
|
guard let device = self.videoDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transaction(device) { device in
|
||||||
|
device.videoZoomFactor = max(device.neutralZoomFactor, min(10.0, device.neutralZoomFactor + zoomLevel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setZoomDelta(_ zoomDelta: CGFloat) {
|
||||||
|
guard let device = self.videoDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transaction(device) { device in
|
||||||
|
device.videoZoomFactor = max(1.0, min(10.0, device.videoZoomFactor * zoomDelta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetZoom(neutral: Bool = true) {
|
||||||
|
guard let device = self.videoDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.transaction(device) { device in
|
||||||
|
device.videoZoomFactor = neutral ? device.neutralZoomFactor : device.minAvailableVideoZoomFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
class CameraInput {
|
class CameraInput {
|
||||||
private var videoInput: AVCaptureDeviceInput?
|
var videoInput: AVCaptureDeviceInput?
|
||||||
private var audioInput: AVCaptureDeviceInput?
|
private var audioInput: AVCaptureDeviceInput?
|
||||||
|
|
||||||
func configure(for session: AVCaptureSession, device: CameraDevice, audio: Bool) {
|
func configure(for session: CameraSession, device: CameraDevice, audio: Bool) {
|
||||||
if let videoDevice = device.videoDevice {
|
if let videoDevice = device.videoDevice {
|
||||||
self.configureVideoInput(for: session, device: videoDevice)
|
self.configureVideoInput(for: session, device: videoDevice)
|
||||||
}
|
}
|
||||||
@ -13,32 +14,42 @@ class CameraInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidate(for session: AVCaptureSession) {
|
func invalidate(for session: CameraSession) {
|
||||||
for input in session.inputs {
|
for input in session.session.inputs {
|
||||||
session.removeInput(input)
|
session.session.removeInput(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureVideoInput(for session: AVCaptureSession, device: AVCaptureDevice) {
|
private func configureVideoInput(for session: CameraSession, device: AVCaptureDevice) {
|
||||||
|
if let currentVideoInput = self.videoInput {
|
||||||
|
session.session.removeInput(currentVideoInput)
|
||||||
|
self.videoInput = nil
|
||||||
|
}
|
||||||
if let videoInput = try? AVCaptureDeviceInput(device: device) {
|
if let videoInput = try? AVCaptureDeviceInput(device: device) {
|
||||||
if let currentVideoInput = self.videoInput {
|
|
||||||
session.removeInput(currentVideoInput)
|
|
||||||
}
|
|
||||||
self.videoInput = videoInput
|
self.videoInput = videoInput
|
||||||
if session.canAddInput(videoInput) {
|
if session.session.canAddInput(videoInput) {
|
||||||
session.addInput(videoInput)
|
if session.hasMultiCam {
|
||||||
|
session.session.addInputWithNoConnections(videoInput)
|
||||||
|
} else {
|
||||||
|
session.session.addInput(videoInput)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add video input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureAudioInput(for session: AVCaptureSession, device: AVCaptureDevice) {
|
private func configureAudioInput(for session: CameraSession, device: AVCaptureDevice) {
|
||||||
guard self.audioInput == nil else {
|
if let currentAudioInput = self.audioInput {
|
||||||
return
|
session.session.removeInput(currentAudioInput)
|
||||||
|
self.audioInput = nil
|
||||||
}
|
}
|
||||||
if let audioInput = try? AVCaptureDeviceInput(device: device) {
|
if let audioInput = try? AVCaptureDeviceInput(device: device) {
|
||||||
self.audioInput = audioInput
|
self.audioInput = audioInput
|
||||||
if session.canAddInput(audioInput) {
|
if session.session.canAddInput(audioInput) {
|
||||||
session.addInput(audioInput)
|
session.session.addInput(audioInput)
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add audio input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
390
submodules/Camera/Sources/CameraMetrics.swift
Normal file
390
submodules/Camera/Sources/CameraMetrics.swift
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension Camera {
|
||||||
|
enum Metrics {
|
||||||
|
case singleCamera
|
||||||
|
case iPhone14
|
||||||
|
case iPhone14Plus
|
||||||
|
case iPhone14Pro
|
||||||
|
case iPhone14ProMax
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
init(model: DeviceModel) {
|
||||||
|
switch model {
|
||||||
|
case .iPodTouch1, .iPodTouch2, .iPodTouch3, .iPodTouch4, .iPodTouch5, .iPodTouch6, .iPodTouch7:
|
||||||
|
self = .singleCamera
|
||||||
|
case .iPhone14:
|
||||||
|
self = .iPhone14
|
||||||
|
case .iPhone14Plus:
|
||||||
|
self = .iPhone14Plus
|
||||||
|
case .iPhone14Pro:
|
||||||
|
self = .iPhone14Pro
|
||||||
|
case .iPhone14ProMax:
|
||||||
|
self = .iPhone14ProMax
|
||||||
|
case .unknown:
|
||||||
|
self = .unknown
|
||||||
|
default:
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var zoomLevels: [Float] {
|
||||||
|
switch self {
|
||||||
|
case .singleCamera:
|
||||||
|
return [1.0]
|
||||||
|
case .iPhone14:
|
||||||
|
return [0.5, 1.0, 2.0]
|
||||||
|
case .iPhone14Plus:
|
||||||
|
return [0.5, 1.0, 2.0]
|
||||||
|
case .iPhone14Pro:
|
||||||
|
return [0.5, 1.0, 2.0, 3.0]
|
||||||
|
case .iPhone14ProMax:
|
||||||
|
return [0.5, 1.0, 2.0, 3.0]
|
||||||
|
case .unknown:
|
||||||
|
return [1.0, 2.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceModel: CaseIterable, Equatable {
|
||||||
|
static var allCases: [DeviceModel] {
|
||||||
|
return [
|
||||||
|
.iPodTouch1,
|
||||||
|
.iPodTouch2,
|
||||||
|
.iPodTouch3,
|
||||||
|
.iPodTouch4,
|
||||||
|
.iPodTouch5,
|
||||||
|
.iPodTouch6,
|
||||||
|
.iPodTouch7,
|
||||||
|
.iPhone,
|
||||||
|
.iPhone3G,
|
||||||
|
.iPhone3GS,
|
||||||
|
.iPhone4,
|
||||||
|
.iPhone4S,
|
||||||
|
.iPhone5,
|
||||||
|
.iPhone5C,
|
||||||
|
.iPhone5S,
|
||||||
|
.iPhone6,
|
||||||
|
.iPhone6Plus,
|
||||||
|
.iPhone6S,
|
||||||
|
.iPhone6SPlus,
|
||||||
|
.iPhoneSE,
|
||||||
|
.iPhone7,
|
||||||
|
.iPhone7Plus,
|
||||||
|
.iPhone8,
|
||||||
|
.iPhone8Plus,
|
||||||
|
.iPhoneX,
|
||||||
|
.iPhoneXS,
|
||||||
|
.iPhoneXR,
|
||||||
|
.iPhone11,
|
||||||
|
.iPhone11Pro,
|
||||||
|
.iPhone11ProMax,
|
||||||
|
.iPhone12,
|
||||||
|
.iPhone12Mini,
|
||||||
|
.iPhone12Pro,
|
||||||
|
.iPhone12ProMax,
|
||||||
|
.iPhone13,
|
||||||
|
.iPhone13Mini,
|
||||||
|
.iPhone13Pro,
|
||||||
|
.iPhone13ProMax,
|
||||||
|
.iPhone14,
|
||||||
|
.iPhone14Plus,
|
||||||
|
.iPhone14Pro,
|
||||||
|
.iPhone14ProMax
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
case iPodTouch1
|
||||||
|
case iPodTouch2
|
||||||
|
case iPodTouch3
|
||||||
|
case iPodTouch4
|
||||||
|
case iPodTouch5
|
||||||
|
case iPodTouch6
|
||||||
|
case iPodTouch7
|
||||||
|
|
||||||
|
case iPhone
|
||||||
|
case iPhone3G
|
||||||
|
case iPhone3GS
|
||||||
|
|
||||||
|
case iPhone4
|
||||||
|
case iPhone4S
|
||||||
|
|
||||||
|
case iPhone5
|
||||||
|
case iPhone5C
|
||||||
|
case iPhone5S
|
||||||
|
|
||||||
|
case iPhone6
|
||||||
|
case iPhone6Plus
|
||||||
|
case iPhone6S
|
||||||
|
case iPhone6SPlus
|
||||||
|
|
||||||
|
case iPhoneSE
|
||||||
|
|
||||||
|
case iPhone7
|
||||||
|
case iPhone7Plus
|
||||||
|
case iPhone8
|
||||||
|
case iPhone8Plus
|
||||||
|
|
||||||
|
case iPhoneX
|
||||||
|
case iPhoneXS
|
||||||
|
case iPhoneXSMax
|
||||||
|
case iPhoneXR
|
||||||
|
|
||||||
|
case iPhone11
|
||||||
|
case iPhone11Pro
|
||||||
|
case iPhone11ProMax
|
||||||
|
|
||||||
|
case iPhoneSE2ndGen
|
||||||
|
|
||||||
|
case iPhone12
|
||||||
|
case iPhone12Mini
|
||||||
|
case iPhone12Pro
|
||||||
|
case iPhone12ProMax
|
||||||
|
|
||||||
|
case iPhone13
|
||||||
|
case iPhone13Mini
|
||||||
|
case iPhone13Pro
|
||||||
|
case iPhone13ProMax
|
||||||
|
|
||||||
|
case iPhoneSE3rdGen
|
||||||
|
|
||||||
|
case iPhone14
|
||||||
|
case iPhone14Plus
|
||||||
|
case iPhone14Pro
|
||||||
|
case iPhone14ProMax
|
||||||
|
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
var modelId: [String] {
|
||||||
|
switch self {
|
||||||
|
case .iPodTouch1:
|
||||||
|
return ["iPod1,1"]
|
||||||
|
case .iPodTouch2:
|
||||||
|
return ["iPod2,1"]
|
||||||
|
case .iPodTouch3:
|
||||||
|
return ["iPod3,1"]
|
||||||
|
case .iPodTouch4:
|
||||||
|
return ["iPod4,1"]
|
||||||
|
case .iPodTouch5:
|
||||||
|
return ["iPod5,1"]
|
||||||
|
case .iPodTouch6:
|
||||||
|
return ["iPod7,1"]
|
||||||
|
case .iPodTouch7:
|
||||||
|
return ["iPod9,1"]
|
||||||
|
case .iPhone:
|
||||||
|
return ["iPhone1,1"]
|
||||||
|
case .iPhone3G:
|
||||||
|
return ["iPhone1,2"]
|
||||||
|
case .iPhone3GS:
|
||||||
|
return ["iPhone2,1"]
|
||||||
|
case .iPhone4:
|
||||||
|
return ["iPhone3,1", "iPhone3,2", "iPhone3,3"]
|
||||||
|
case .iPhone4S:
|
||||||
|
return ["iPhone4,1", "iPhone4,2", "iPhone4,3"]
|
||||||
|
case .iPhone5:
|
||||||
|
return ["iPhone5,1", "iPhone5,2"]
|
||||||
|
case .iPhone5C:
|
||||||
|
return ["iPhone5,3", "iPhone5,4"]
|
||||||
|
case .iPhone5S:
|
||||||
|
return ["iPhone6,1", "iPhone6,2"]
|
||||||
|
case .iPhone6:
|
||||||
|
return ["iPhone7,2"]
|
||||||
|
case .iPhone6Plus:
|
||||||
|
return ["iPhone7,1"]
|
||||||
|
case .iPhone6S:
|
||||||
|
return ["iPhone8,1"]
|
||||||
|
case .iPhone6SPlus:
|
||||||
|
return ["iPhone8,2"]
|
||||||
|
case .iPhoneSE:
|
||||||
|
return ["iPhone8,4"]
|
||||||
|
case .iPhone7:
|
||||||
|
return ["iPhone9,1", "iPhone9,3"]
|
||||||
|
case .iPhone7Plus:
|
||||||
|
return ["iPhone9,2", "iPhone9,4"]
|
||||||
|
case .iPhone8:
|
||||||
|
return ["iPhone10,1", "iPhone10,4"]
|
||||||
|
case .iPhone8Plus:
|
||||||
|
return ["iPhone10,2", "iPhone10,5"]
|
||||||
|
case .iPhoneX:
|
||||||
|
return ["iPhone10,3", "iPhone10,6"]
|
||||||
|
case .iPhoneXS:
|
||||||
|
return ["iPhone11,2"]
|
||||||
|
case .iPhoneXSMax:
|
||||||
|
return ["iPhone11,4", "iPhone11,6"]
|
||||||
|
case .iPhoneXR:
|
||||||
|
return ["iPhone11,8"]
|
||||||
|
case .iPhone11:
|
||||||
|
return ["iPhone12,1"]
|
||||||
|
case .iPhone11Pro:
|
||||||
|
return ["iPhone12,3"]
|
||||||
|
case .iPhone11ProMax:
|
||||||
|
return ["iPhone12,5"]
|
||||||
|
case .iPhoneSE2ndGen:
|
||||||
|
return ["iPhone12,8"]
|
||||||
|
case .iPhone12:
|
||||||
|
return ["iPhone13,2"]
|
||||||
|
case .iPhone12Mini:
|
||||||
|
return ["iPhone13,1"]
|
||||||
|
case .iPhone12Pro:
|
||||||
|
return ["iPhone13,3"]
|
||||||
|
case .iPhone12ProMax:
|
||||||
|
return ["iPhone13,4"]
|
||||||
|
case .iPhone13:
|
||||||
|
return ["iPhone14,5"]
|
||||||
|
case .iPhone13Mini:
|
||||||
|
return ["iPhone14,4"]
|
||||||
|
case .iPhone13Pro:
|
||||||
|
return ["iPhone14,2"]
|
||||||
|
case .iPhone13ProMax:
|
||||||
|
return ["iPhone14,3"]
|
||||||
|
case .iPhoneSE3rdGen:
|
||||||
|
return ["iPhone14,6"]
|
||||||
|
case .iPhone14:
|
||||||
|
return ["iPhone14,7"]
|
||||||
|
case .iPhone14Plus:
|
||||||
|
return ["iPhone14,8"]
|
||||||
|
case .iPhone14Pro:
|
||||||
|
return ["iPhone15,2"]
|
||||||
|
case .iPhone14ProMax:
|
||||||
|
return ["iPhone15,3"]
|
||||||
|
case let .unknown(modelId):
|
||||||
|
return [modelId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelName: String {
|
||||||
|
switch self {
|
||||||
|
case .iPodTouch1:
|
||||||
|
return "iPod touch 1G"
|
||||||
|
case .iPodTouch2:
|
||||||
|
return "iPod touch 2G"
|
||||||
|
case .iPodTouch3:
|
||||||
|
return "iPod touch 3G"
|
||||||
|
case .iPodTouch4:
|
||||||
|
return "iPod touch 4G"
|
||||||
|
case .iPodTouch5:
|
||||||
|
return "iPod touch 5G"
|
||||||
|
case .iPodTouch6:
|
||||||
|
return "iPod touch 6G"
|
||||||
|
case .iPodTouch7:
|
||||||
|
return "iPod touch 7G"
|
||||||
|
case .iPhone:
|
||||||
|
return "iPhone"
|
||||||
|
case .iPhone3G:
|
||||||
|
return "iPhone 3G"
|
||||||
|
case .iPhone3GS:
|
||||||
|
return "iPhone 3GS"
|
||||||
|
case .iPhone4:
|
||||||
|
return "iPhone 4"
|
||||||
|
case .iPhone4S:
|
||||||
|
return "iPhone 4S"
|
||||||
|
case .iPhone5:
|
||||||
|
return "iPhone 5"
|
||||||
|
case .iPhone5C:
|
||||||
|
return "iPhone 5C"
|
||||||
|
case .iPhone5S:
|
||||||
|
return "iPhone 5S"
|
||||||
|
case .iPhone6:
|
||||||
|
return "iPhone 6"
|
||||||
|
case .iPhone6Plus:
|
||||||
|
return "iPhone 6 Plus"
|
||||||
|
case .iPhone6S:
|
||||||
|
return "iPhone 6S"
|
||||||
|
case .iPhone6SPlus:
|
||||||
|
return "iPhone 6S Plus"
|
||||||
|
case .iPhoneSE:
|
||||||
|
return "iPhone SE"
|
||||||
|
case .iPhone7:
|
||||||
|
return "iPhone 7"
|
||||||
|
case .iPhone7Plus:
|
||||||
|
return "iPhone 7 Plus"
|
||||||
|
case .iPhone8:
|
||||||
|
return "iPhone 8"
|
||||||
|
case .iPhone8Plus:
|
||||||
|
return "iPhone 8 Plus"
|
||||||
|
case .iPhoneX:
|
||||||
|
return "iPhone X"
|
||||||
|
case .iPhoneXS:
|
||||||
|
return "iPhone XS"
|
||||||
|
case .iPhoneXSMax:
|
||||||
|
return "iPhone XS Max"
|
||||||
|
case .iPhoneXR:
|
||||||
|
return "iPhone XR"
|
||||||
|
case .iPhone11:
|
||||||
|
return "iPhone 11"
|
||||||
|
case .iPhone11Pro:
|
||||||
|
return "iPhone 11 Pro"
|
||||||
|
case .iPhone11ProMax:
|
||||||
|
return "iPhone 11 Pro Max"
|
||||||
|
case .iPhoneSE2ndGen:
|
||||||
|
return "iPhone SE (2nd gen)"
|
||||||
|
case .iPhone12:
|
||||||
|
return "iPhone 12"
|
||||||
|
case .iPhone12Mini:
|
||||||
|
return "iPhone 12 mini"
|
||||||
|
case .iPhone12Pro:
|
||||||
|
return "iPhone 12 Pro"
|
||||||
|
case .iPhone12ProMax:
|
||||||
|
return "iPhone 12 Pro Max"
|
||||||
|
case .iPhone13:
|
||||||
|
return "iPhone 13"
|
||||||
|
case .iPhone13Mini:
|
||||||
|
return "iPhone 13 mini"
|
||||||
|
case .iPhone13Pro:
|
||||||
|
return "iPhone 13 Pro"
|
||||||
|
case .iPhone13ProMax:
|
||||||
|
return "iPhone 13 Pro Max"
|
||||||
|
case .iPhoneSE3rdGen:
|
||||||
|
return "iPhone SE (3rd gen)"
|
||||||
|
case .iPhone14:
|
||||||
|
return "iPhone 14"
|
||||||
|
case .iPhone14Plus:
|
||||||
|
return "iPhone 14 Plus"
|
||||||
|
case .iPhone14Pro:
|
||||||
|
return "iPhone 14 Pro"
|
||||||
|
case .iPhone14ProMax:
|
||||||
|
return "iPhone 14 Pro Max"
|
||||||
|
case let .unknown(modelId):
|
||||||
|
if modelId.hasPrefix("iPhone") {
|
||||||
|
return "Unknown iPhone"
|
||||||
|
} else if modelId.hasPrefix("iPod") {
|
||||||
|
return "Unknown iPod"
|
||||||
|
} else if modelId.hasPrefix("iPad") {
|
||||||
|
return "Unknown iPad"
|
||||||
|
} else {
|
||||||
|
return "Unknown Device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isIpad: Bool {
|
||||||
|
return self.modelId.first?.hasPrefix("iPad") ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
static let current = DeviceModel()
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
var systemInfo = utsname()
|
||||||
|
uname(&systemInfo)
|
||||||
|
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
||||||
|
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
||||||
|
ptr in String.init(validatingUTF8: ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result: DeviceModel?
|
||||||
|
if let modelCode {
|
||||||
|
for model in DeviceModel.allCases {
|
||||||
|
if model.modelId.contains(modelCode) {
|
||||||
|
result = model
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let result {
|
||||||
|
self = result
|
||||||
|
} else {
|
||||||
|
self = .unknown(modelCode ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,36 @@
|
|||||||
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import CoreImage
|
||||||
|
import Vision
|
||||||
|
import VideoToolbox
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
public enum VideoCaptureResult: Equatable {
|
||||||
|
case finished((String, UIImage, Bool, CGSize), (String, UIImage, Bool, CGSize)?, Double, [(Bool, Double)], Double)
|
||||||
|
case failed
|
||||||
|
|
||||||
|
public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case .failed:
|
||||||
|
if case .failed = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .finished(_, _, lhsDuration, lhsChangeTimestamps, lhsTime):
|
||||||
|
if case let .finished(_, _, rhsDuration, rhsChangeTimestamps, rhsTime) = rhs, lhsDuration == rhsDuration, lhsTime == rhsTime {
|
||||||
|
if lhsChangeTimestamps.count != rhsChangeTimestamps.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct CameraCode: Equatable {
|
public struct CameraCode: Equatable {
|
||||||
public enum CodeType {
|
public enum CodeType {
|
||||||
@ -39,21 +71,32 @@ public struct CameraCode: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class CameraOutput: NSObject {
|
final class CameraOutput: NSObject {
|
||||||
//private let photoOutput = CameraPhotoOutput()
|
let photoOutput = AVCapturePhotoOutput()
|
||||||
private let videoOutput = AVCaptureVideoDataOutput()
|
let videoOutput = AVCaptureVideoDataOutput()
|
||||||
private let audioOutput = AVCaptureAudioDataOutput()
|
let audioOutput = AVCaptureAudioDataOutput()
|
||||||
private let metadataOutput = AVCaptureMetadataOutput()
|
let metadataOutput = AVCaptureMetadataOutput()
|
||||||
|
|
||||||
|
let exclusive: Bool
|
||||||
|
|
||||||
|
private var photoConnection: AVCaptureConnection?
|
||||||
|
private var videoConnection: AVCaptureConnection?
|
||||||
|
private var previewConnection: AVCaptureConnection?
|
||||||
|
|
||||||
private let queue = DispatchQueue(label: "")
|
private let queue = DispatchQueue(label: "")
|
||||||
private let metadataQueue = DispatchQueue(label: "")
|
private let metadataQueue = DispatchQueue(label: "")
|
||||||
|
|
||||||
var processSampleBuffer: ((CMSampleBuffer, AVCaptureConnection) -> Void)?
|
private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:]
|
||||||
|
private var videoRecorder: VideoRecorder?
|
||||||
|
|
||||||
|
var processSampleBuffer: ((CMSampleBuffer, CVImageBuffer, AVCaptureConnection) -> Void)?
|
||||||
var processCodes: (([CameraCode]) -> Void)?
|
var processCodes: (([CameraCode]) -> Void)?
|
||||||
|
|
||||||
override init() {
|
init(exclusive: Bool) {
|
||||||
super.init()
|
self.exclusive = exclusive
|
||||||
|
|
||||||
self.videoOutput.alwaysDiscardsLateVideoFrames = true;
|
super.init()
|
||||||
|
|
||||||
|
self.videoOutput.alwaysDiscardsLateVideoFrames = false
|
||||||
self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any]
|
self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,28 +105,248 @@ final class CameraOutput: NSObject {
|
|||||||
self.audioOutput.setSampleBufferDelegate(nil, queue: nil)
|
self.audioOutput.setSampleBufferDelegate(nil, queue: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(for session: AVCaptureSession) {
|
func configure(for session: CameraSession, device: CameraDevice, input: CameraInput, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
|
||||||
if session.canAddOutput(self.videoOutput) {
|
if session.session.canAddOutput(self.videoOutput) {
|
||||||
session.addOutput(self.videoOutput)
|
if session.hasMultiCam {
|
||||||
|
session.session.addOutputWithNoConnections(self.videoOutput)
|
||||||
|
} else {
|
||||||
|
session.session.addOutput(self.videoOutput)
|
||||||
|
}
|
||||||
self.videoOutput.setSampleBufferDelegate(self, queue: self.queue)
|
self.videoOutput.setSampleBufferDelegate(self, queue: self.queue)
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add video output")
|
||||||
}
|
}
|
||||||
if session.canAddOutput(self.audioOutput) {
|
if audio, session.session.canAddOutput(self.audioOutput) {
|
||||||
session.addOutput(self.audioOutput)
|
session.session.addOutput(self.audioOutput)
|
||||||
self.audioOutput.setSampleBufferDelegate(self, queue: self.queue)
|
self.audioOutput.setSampleBufferDelegate(self, queue: self.queue)
|
||||||
}
|
}
|
||||||
if session.canAddOutput(self.metadataOutput) {
|
if photo, session.session.canAddOutput(self.photoOutput) {
|
||||||
session.addOutput(self.metadataOutput)
|
if session.hasMultiCam {
|
||||||
|
session.session.addOutputWithNoConnections(self.photoOutput)
|
||||||
|
} else {
|
||||||
|
session.session.addOutput(self.photoOutput)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add photo output")
|
||||||
|
}
|
||||||
|
if metadata, session.session.canAddOutput(self.metadataOutput) {
|
||||||
|
session.session.addOutput(self.metadataOutput)
|
||||||
|
|
||||||
self.metadataOutput.setMetadataObjectsDelegate(self, queue: self.metadataQueue)
|
self.metadataOutput.setMetadataObjectsDelegate(self, queue: self.metadataQueue)
|
||||||
if self.metadataOutput.availableMetadataObjectTypes.contains(.qr) {
|
if self.metadataOutput.availableMetadataObjectTypes.contains(.qr) {
|
||||||
self.metadataOutput.metadataObjectTypes = [.qr]
|
self.metadataOutput.metadataObjectTypes = [.qr]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *), session.hasMultiCam {
|
||||||
|
if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) {
|
||||||
|
if let previewView {
|
||||||
|
let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: previewView.videoPreviewLayer)
|
||||||
|
if session.session.canAddConnection(previewConnection) {
|
||||||
|
session.session.addConnection(previewConnection)
|
||||||
|
self.previewConnection = previewConnection
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add preview connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoConnection = AVCaptureConnection(inputPorts: ports, output: self.videoOutput)
|
||||||
|
if session.session.canAddConnection(videoConnection) {
|
||||||
|
session.session.addConnection(videoConnection)
|
||||||
|
self.videoConnection = videoConnection
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't add video connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if photo {
|
||||||
|
let photoConnection = AVCaptureConnection(inputPorts: ports, output: self.photoOutput)
|
||||||
|
if session.session.canAddConnection(photoConnection) {
|
||||||
|
session.session.addConnection(photoConnection)
|
||||||
|
self.photoConnection = photoConnection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Camera", "Can't get video port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate(for session: CameraSession) {
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
if let previewConnection = self.previewConnection {
|
||||||
|
if session.session.connections.contains(where: { $0 === previewConnection }) {
|
||||||
|
session.session.removeConnection(previewConnection)
|
||||||
|
}
|
||||||
|
self.previewConnection = nil
|
||||||
|
}
|
||||||
|
if let videoConnection = self.videoConnection {
|
||||||
|
if session.session.connections.contains(where: { $0 === videoConnection }) {
|
||||||
|
session.session.removeConnection(videoConnection)
|
||||||
|
}
|
||||||
|
self.videoConnection = nil
|
||||||
|
}
|
||||||
|
if let photoConnection = self.photoConnection {
|
||||||
|
if session.session.connections.contains(where: { $0 === photoConnection }) {
|
||||||
|
session.session.removeConnection(photoConnection)
|
||||||
|
}
|
||||||
|
self.photoConnection = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if session.session.outputs.contains(where: { $0 === self.videoOutput }) {
|
||||||
|
session.session.removeOutput(self.videoOutput)
|
||||||
|
}
|
||||||
|
if session.session.outputs.contains(where: { $0 === self.audioOutput }) {
|
||||||
|
session.session.removeOutput(self.audioOutput)
|
||||||
|
}
|
||||||
|
if session.session.outputs.contains(where: { $0 === self.photoOutput }) {
|
||||||
|
session.session.removeOutput(self.photoOutput)
|
||||||
|
}
|
||||||
|
if session.session.outputs.contains(where: { $0 === self.metadataOutput }) {
|
||||||
|
session.session.removeOutput(self.metadataOutput)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidate(for session: AVCaptureSession) {
|
func configureVideoStabilization() {
|
||||||
for output in session.outputs {
|
if let videoDataOutputConnection = self.videoOutput.connection(with: .video), videoDataOutputConnection.isVideoStabilizationSupported {
|
||||||
session.removeOutput(output)
|
videoDataOutputConnection.preferredVideoStabilizationMode = .standard
|
||||||
|
// if #available(iOS 13.0, *) {
|
||||||
|
// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended
|
||||||
|
// } else {
|
||||||
|
// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFlashActive: Signal<Bool, NoError> {
|
||||||
|
return Signal { [weak self] subscriber in
|
||||||
|
guard let self else {
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
subscriber.putNext(self.photoOutput.isFlashScene)
|
||||||
|
let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { device, _ in
|
||||||
|
subscriber.putNext(self.photoOutput.isFlashScene)
|
||||||
|
})
|
||||||
|
return ActionDisposable {
|
||||||
|
observer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
func takePhoto(orientation: AVCaptureVideoOrientation, flashMode: AVCaptureDevice.FlashMode) -> Signal<PhotoCaptureResult, NoError> {
|
||||||
|
var mirror = false
|
||||||
|
if let connection = self.photoOutput.connection(with: .video) {
|
||||||
|
connection.videoOrientation = orientation
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = AVCapturePhotoSettings(format: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)])
|
||||||
|
settings.flashMode = flashMode
|
||||||
|
if let previewPhotoPixelFormatType = settings.availablePreviewPhotoPixelFormatTypes.first {
|
||||||
|
settings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType]
|
||||||
|
}
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
if self.exclusive {
|
||||||
|
if self.photoOutput.maxPhotoQualityPrioritization != .speed {
|
||||||
|
settings.photoQualityPrioritization = .balanced
|
||||||
|
} else {
|
||||||
|
settings.photoQualityPrioritization = .speed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settings.photoQualityPrioritization = .speed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uniqueId = settings.uniqueID
|
||||||
|
let photoCapture = PhotoCaptureContext(settings: settings, orientation: orientation, mirror: mirror)
|
||||||
|
self.photoCaptureRequests[uniqueId] = photoCapture
|
||||||
|
self.photoOutput.capturePhoto(with: settings, delegate: photoCapture)
|
||||||
|
|
||||||
|
return photoCapture.signal
|
||||||
|
|> afterDisposed { [weak self] in
|
||||||
|
self?.photoCaptureRequests.removeValue(forKey: uniqueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isRecording: Bool {
|
||||||
|
return self.videoRecorder != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
|
||||||
|
func startRecording(isDualCamera: Bool, position: Camera.Position? = nil, orientation: AVCaptureVideoOrientation) -> Signal<Double, NoError> {
|
||||||
|
guard self.videoRecorder == nil else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
let codecType: AVVideoCodecType
|
||||||
|
if hasHEVCHardwareEncoder {
|
||||||
|
codecType = .hevc
|
||||||
|
} else {
|
||||||
|
codecType = .h264
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) ?? [:]
|
||||||
|
|
||||||
|
var dimensions: CGSize = CGSize(width: 1080, height: 1920)
|
||||||
|
if orientation == .landscapeLeft {
|
||||||
|
dimensions = CGSize(width: 1920, height: 1080)
|
||||||
|
} else if orientation == .landscapeRight {
|
||||||
|
dimensions = CGSize(width: 1920, height: 1080)
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputFileName = NSUUID().uuidString
|
||||||
|
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
|
||||||
|
let outputFileURL = URL(fileURLWithPath: outputFilePath)
|
||||||
|
|
||||||
|
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), orientation: orientation, fileUrl: outputFileURL, completion: { [weak self] result in
|
||||||
|
if case let .success(transitionImage, duration, positionChangeTimestamps) = result {
|
||||||
|
self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage ?? UIImage(), false, dimensions), nil, duration, positionChangeTimestamps.map { ($0 == .front, $1) }, CACurrentMediaTime()))
|
||||||
|
} else {
|
||||||
|
self?.recordingCompletionPipe.putNext(.failed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
videoRecorder?.start()
|
||||||
|
self.videoRecorder = videoRecorder
|
||||||
|
|
||||||
|
if isDualCamera, let position {
|
||||||
|
videoRecorder?.markPositionChange(position: position, time: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Signal { subscriber in
|
||||||
|
let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak videoRecorder] in
|
||||||
|
subscriber.putNext(videoRecorder?.duration ?? 0.0)
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||||
|
guard let videoRecorder = self.videoRecorder, videoRecorder.isRecording else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
videoRecorder.stop()
|
||||||
|
|
||||||
|
return self.recordingCompletionPipe.signal()
|
||||||
|
|> take(1)
|
||||||
|
|> afterDisposed {
|
||||||
|
self.videoRecorder = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func markPositionChange(position: Camera.Position) {
|
||||||
|
if let videoRecorder = self.videoRecorder {
|
||||||
|
videoRecorder.markPositionChange(position: position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +357,13 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.processSampleBuffer?(sampleBuffer, connection)
|
if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||||
|
self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let videoRecorder = self.videoRecorder, videoRecorder.isRecording {
|
||||||
|
videoRecorder.appendSampleBuffer(sampleBuffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
||||||
@ -118,3 +387,14 @@ extension CameraOutput: AVCaptureMetadataOutputObjectsDelegate {
|
|||||||
self.processCodes?(codes)
|
self.processCodes?(codes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let hasHEVCHardwareEncoder: Bool = {
|
||||||
|
let spec: [CFString: Any] = [:]
|
||||||
|
var outID: CFString?
|
||||||
|
var properties: CFDictionary?
|
||||||
|
let result = VTCopySupportedPropertyDictionaryForEncoder(width: 1920, height: 1080, codecType: kCMVideoCodecType_HEVC, encoderSpecification: spec as CFDictionary, encoderIDOut: &outID, supportedPropertiesOut: &properties)
|
||||||
|
if result == kVTCouldNotFindVideoEncoderErr {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return result == noErr
|
||||||
|
}()
|
||||||
|
655
submodules/Camera/Sources/CameraPreviewView.swift
Normal file
655
submodules/Camera/Sources/CameraPreviewView.swift
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AVFoundation
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Metal
|
||||||
|
import MetalKit
|
||||||
|
import CoreMedia
|
||||||
|
import Vision
|
||||||
|
import ImageBlur
|
||||||
|
|
||||||
|
private extension UIInterfaceOrientation {
|
||||||
|
var videoOrientation: AVCaptureVideoOrientation {
|
||||||
|
switch self {
|
||||||
|
case .portraitUpsideDown: return .portraitUpsideDown
|
||||||
|
case .landscapeRight: return .landscapeRight
|
||||||
|
case .landscapeLeft: return .landscapeLeft
|
||||||
|
case .portrait: return .portrait
|
||||||
|
default: return .portrait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CameraSimplePreviewView: UIView {
|
||||||
|
func updateOrientation() {
|
||||||
|
guard self.videoPreviewLayer.connection?.isVideoOrientationSupported == true else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let statusBarOrientation: UIInterfaceOrientation
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
statusBarOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .portrait
|
||||||
|
} else {
|
||||||
|
statusBarOrientation = UIApplication.shared.statusBarOrientation
|
||||||
|
}
|
||||||
|
let videoOrientation = statusBarOrientation.videoOrientation
|
||||||
|
self.videoPreviewLayer.connection?.videoOrientation = videoOrientation
|
||||||
|
self.videoPreviewLayer.removeAllAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func lastBackImage() -> UIImage {
|
||||||
|
let imagePath = NSTemporaryDirectory() + "backCameraImage.jpg"
|
||||||
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) {
|
||||||
|
return image
|
||||||
|
} else {
|
||||||
|
return UIImage(bundleImageName: "Camera/Placeholder")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveLastBackImage(_ image: UIImage) {
|
||||||
|
let imagePath = NSTemporaryDirectory() + "backCameraImage.jpg"
|
||||||
|
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||||
|
try? data.write(to: URL(fileURLWithPath: imagePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func lastFrontImage() -> UIImage {
|
||||||
|
let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg"
|
||||||
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) {
|
||||||
|
return image
|
||||||
|
} else {
|
||||||
|
return UIImage(bundleImageName: "Camera/SelfiePlaceholder")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveLastFrontImage(_ image: UIImage) {
|
||||||
|
let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg"
|
||||||
|
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||||
|
try? data.write(to: URL(fileURLWithPath: imagePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var previewingDisposable: Disposable?
|
||||||
|
private let placeholderView = UIImageView()
|
||||||
|
|
||||||
|
public init(frame: CGRect, main: Bool) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.videoPreviewLayer.videoGravity = main ? .resizeAspectFill : .resizeAspect
|
||||||
|
self.placeholderView.contentMode = main ? .scaleAspectFill : .scaleAspectFit
|
||||||
|
|
||||||
|
self.addSubview(self.placeholderView)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.previewingDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.updateOrientation()
|
||||||
|
self.placeholderView.frame = self.bounds.insetBy(dx: -1.0, dy: -1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removePlaceholder(delay: Double = 0.0) {
|
||||||
|
UIView.animate(withDuration: 0.3, delay: delay) {
|
||||||
|
self.placeholderView.alpha = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resetPlaceholder(front: Bool) {
|
||||||
|
self.placeholderView.image = front ? CameraSimplePreviewView.lastFrontImage() : CameraSimplePreviewView.lastBackImage()
|
||||||
|
self.placeholderView.alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _videoPreviewLayer: AVCaptureVideoPreviewLayer?
|
||||||
|
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
||||||
|
if let layer = self._videoPreviewLayer {
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
guard let layer = self.layer as? AVCaptureVideoPreviewLayer else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
self._videoPreviewLayer = layer
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
self.videoPreviewLayer.session = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSession(_ session: AVCaptureSession, autoConnect: Bool) {
|
||||||
|
if autoConnect {
|
||||||
|
self.videoPreviewLayer.session = session
|
||||||
|
} else {
|
||||||
|
self.videoPreviewLayer.setSessionWithNoConnection(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isEnabled: Bool = true {
|
||||||
|
didSet {
|
||||||
|
self.videoPreviewLayer.connection?.isEnabled = self.isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override class var layerClass: AnyClass {
|
||||||
|
return AVCaptureVideoPreviewLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
public var isPreviewing: Signal<Bool, NoError> {
|
||||||
|
return Signal { [weak self] subscriber in
|
||||||
|
guard let self else {
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
subscriber.putNext(self.videoPreviewLayer.isPreviewing)
|
||||||
|
let observer = self.videoPreviewLayer.observe(\.isPreviewing, options: [.new], changeHandler: { view, _ in
|
||||||
|
subscriber.putNext(view.isPreviewing)
|
||||||
|
})
|
||||||
|
return ActionDisposable {
|
||||||
|
observer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cameraPoint(for location: CGPoint) -> CGPoint {
|
||||||
|
return self.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CameraPreviewView: MTKView {
|
||||||
|
private let queue = DispatchQueue(label: "CameraPreview", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
|
||||||
|
private let commandQueue: MTLCommandQueue
|
||||||
|
private var textureCache: CVMetalTextureCache?
|
||||||
|
private var sampler: MTLSamplerState!
|
||||||
|
private var renderPipelineState: MTLRenderPipelineState!
|
||||||
|
private var vertexCoordBuffer: MTLBuffer!
|
||||||
|
private var texCoordBuffer: MTLBuffer!
|
||||||
|
|
||||||
|
private var textureWidth: Int = 0
|
||||||
|
private var textureHeight: Int = 0
|
||||||
|
private var textureMirroring = false
|
||||||
|
private var textureRotation: Rotation = .rotate0Degrees
|
||||||
|
|
||||||
|
private var textureTranform: CGAffineTransform?
|
||||||
|
private var _bounds = CGRectNull
|
||||||
|
|
||||||
|
public enum Rotation: Int {
|
||||||
|
case rotate0Degrees
|
||||||
|
case rotate90Degrees
|
||||||
|
case rotate180Degrees
|
||||||
|
case rotate270Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _mirroring: Bool?
|
||||||
|
private var _scheduledMirroring: Bool?
|
||||||
|
public var mirroring = false {
|
||||||
|
didSet {
|
||||||
|
self.queue.sync {
|
||||||
|
if self._mirroring != nil {
|
||||||
|
self._scheduledMirroring = self.mirroring
|
||||||
|
} else {
|
||||||
|
self._mirroring = self.mirroring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _rotation: Rotation = .rotate0Degrees
|
||||||
|
public var rotation: Rotation = .rotate0Degrees {
|
||||||
|
didSet {
|
||||||
|
self.queue.sync {
|
||||||
|
self._rotation = rotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _pixelBuffer: CVPixelBuffer?
|
||||||
|
var pixelBuffer: CVPixelBuffer? {
|
||||||
|
didSet {
|
||||||
|
self.queue.sync {
|
||||||
|
if let scheduledMirroring = self._scheduledMirroring {
|
||||||
|
self._scheduledMirroring = nil
|
||||||
|
self._mirroring = scheduledMirroring
|
||||||
|
}
|
||||||
|
self._pixelBuffer = pixelBuffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(test: Bool) {
|
||||||
|
let mainBundle = Bundle(for: CameraPreviewView.self)
|
||||||
|
|
||||||
|
guard let path = mainBundle.path(forResource: "CameraBundle", ofType: "bundle") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let bundle = Bundle(path: path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let commandQueue = device.makeCommandQueue() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.commandQueue = commandQueue
|
||||||
|
|
||||||
|
super.init(frame: .zero, device: device)
|
||||||
|
|
||||||
|
self.colorPixelFormat = .bgra8Unorm
|
||||||
|
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
|
||||||
|
pipelineDescriptor.vertexFunction = defaultLibrary.makeFunction(name: "vertexPassThrough")
|
||||||
|
pipelineDescriptor.fragmentFunction = defaultLibrary.makeFunction(name: "fragmentPassThrough")
|
||||||
|
|
||||||
|
let samplerDescriptor = MTLSamplerDescriptor()
|
||||||
|
samplerDescriptor.sAddressMode = .clampToEdge
|
||||||
|
samplerDescriptor.tAddressMode = .clampToEdge
|
||||||
|
samplerDescriptor.minFilter = .linear
|
||||||
|
samplerDescriptor.magFilter = .linear
|
||||||
|
self.sampler = device.makeSamplerState(descriptor: samplerDescriptor)
|
||||||
|
|
||||||
|
do {
|
||||||
|
self.renderPipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
} catch {
|
||||||
|
fatalError("\(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setupTextureCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupTextureCache() {
|
||||||
|
var newTextureCache: CVMetalTextureCache?
|
||||||
|
if CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device!, nil, &newTextureCache) == kCVReturnSuccess {
|
||||||
|
self.textureCache = newTextureCache
|
||||||
|
} else {
|
||||||
|
assertionFailure("Unable to allocate texture cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupTransform(width: Int, height: Int, rotation: Rotation, mirroring: Bool) {
|
||||||
|
var scaleX: Float = 1.0
|
||||||
|
var scaleY: Float = 1.0
|
||||||
|
var resizeAspect: Float = 1.0
|
||||||
|
|
||||||
|
self._bounds = self.bounds
|
||||||
|
self.textureWidth = width
|
||||||
|
self.textureHeight = height
|
||||||
|
self.textureMirroring = mirroring
|
||||||
|
self.textureRotation = rotation
|
||||||
|
|
||||||
|
if self.textureWidth > 0 && self.textureHeight > 0 {
|
||||||
|
switch self.textureRotation {
|
||||||
|
case .rotate0Degrees, .rotate180Degrees:
|
||||||
|
scaleX = Float(self._bounds.width / CGFloat(self.textureWidth))
|
||||||
|
scaleY = Float(self._bounds.height / CGFloat(self.textureHeight))
|
||||||
|
|
||||||
|
case .rotate90Degrees, .rotate270Degrees:
|
||||||
|
scaleX = Float(self._bounds.width / CGFloat(self.textureHeight))
|
||||||
|
scaleY = Float(self._bounds.height / CGFloat(self.textureWidth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resizeAspect = min(scaleX, scaleY)
|
||||||
|
if scaleX < scaleY {
|
||||||
|
scaleY = scaleX / scaleY
|
||||||
|
scaleX = 1.0
|
||||||
|
} else {
|
||||||
|
scaleX = scaleY / scaleX
|
||||||
|
scaleY = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.textureMirroring {
|
||||||
|
scaleX *= -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
let vertexData: [Float] = [
|
||||||
|
-scaleX, -scaleY, 0.0, 1.0,
|
||||||
|
scaleX, -scaleY, 0.0, 1.0,
|
||||||
|
-scaleX, scaleY, 0.0, 1.0,
|
||||||
|
scaleX, scaleY, 0.0, 1.0
|
||||||
|
]
|
||||||
|
self.vertexCoordBuffer = device!.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout<Float>.size, options: [])
|
||||||
|
|
||||||
|
var texCoordBufferData: [Float]
|
||||||
|
switch self.textureRotation {
|
||||||
|
case .rotate0Degrees:
|
||||||
|
texCoordBufferData = [
|
||||||
|
0.0, 1.0,
|
||||||
|
1.0, 1.0,
|
||||||
|
0.0, 0.0,
|
||||||
|
1.0, 0.0
|
||||||
|
]
|
||||||
|
case .rotate180Degrees:
|
||||||
|
texCoordBufferData = [
|
||||||
|
1.0, 0.0,
|
||||||
|
0.0, 0.0,
|
||||||
|
1.0, 1.0,
|
||||||
|
0.0, 1.0
|
||||||
|
]
|
||||||
|
case .rotate90Degrees:
|
||||||
|
texCoordBufferData = [
|
||||||
|
1.0, 1.0,
|
||||||
|
1.0, 0.0,
|
||||||
|
0.0, 1.0,
|
||||||
|
0.0, 0.0
|
||||||
|
]
|
||||||
|
case .rotate270Degrees:
|
||||||
|
texCoordBufferData = [
|
||||||
|
0.0, 0.0,
|
||||||
|
0.0, 1.0,
|
||||||
|
1.0, 0.0,
|
||||||
|
1.0, 1.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.texCoordBuffer = device?.makeBuffer(bytes: texCoordBufferData, length: texCoordBufferData.count * MemoryLayout<Float>.size, options: [])
|
||||||
|
|
||||||
|
var transform = CGAffineTransform.identity
|
||||||
|
if self.textureMirroring {
|
||||||
|
transform = transform.concatenating(CGAffineTransform(scaleX: -1, y: 1))
|
||||||
|
transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(self.textureWidth), y: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self.textureRotation {
|
||||||
|
case .rotate0Degrees:
|
||||||
|
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(0)))
|
||||||
|
case .rotate180Degrees:
|
||||||
|
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(Double.pi)))
|
||||||
|
transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(self.textureWidth), y: CGFloat(self.textureHeight)))
|
||||||
|
case .rotate90Degrees:
|
||||||
|
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat(Double.pi) / 2))
|
||||||
|
transform = transform.concatenating(CGAffineTransform(translationX: CGFloat(self.textureHeight), y: 0))
|
||||||
|
case .rotate270Degrees:
|
||||||
|
transform = transform.concatenating(CGAffineTransform(rotationAngle: 3 * CGFloat(Double.pi) / 2))
|
||||||
|
transform = transform.concatenating(CGAffineTransform(translationX: 0, y: CGFloat(self.textureWidth)))
|
||||||
|
}
|
||||||
|
transform = transform.concatenating(CGAffineTransform(scaleX: CGFloat(resizeAspect), y: CGFloat(resizeAspect)))
|
||||||
|
|
||||||
|
let tranformRect = CGRect(origin: .zero, size: CGSize(width: self.textureWidth, height: self.textureHeight)).applying(transform)
|
||||||
|
let xShift = (self._bounds.size.width - tranformRect.size.width) / 2
|
||||||
|
let yShift = (self._bounds.size.height - tranformRect.size.height) / 2
|
||||||
|
transform = transform.concatenating(CGAffineTransform(translationX: xShift, y: yShift))
|
||||||
|
|
||||||
|
self.textureTranform = transform.inverted()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func draw(_ rect: CGRect) {
|
||||||
|
var pixelBuffer: CVPixelBuffer?
|
||||||
|
var mirroring = false
|
||||||
|
var rotation: Rotation = .rotate0Degrees
|
||||||
|
|
||||||
|
self.queue.sync {
|
||||||
|
pixelBuffer = self._pixelBuffer
|
||||||
|
if let mirroringValue = self._mirroring {
|
||||||
|
mirroring = mirroringValue
|
||||||
|
}
|
||||||
|
rotation = self._rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let drawable = currentDrawable, let currentRenderPassDescriptor = currentRenderPassDescriptor, let previewPixelBuffer = pixelBuffer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = CVPixelBufferGetWidth(previewPixelBuffer)
|
||||||
|
let height = CVPixelBufferGetHeight(previewPixelBuffer)
|
||||||
|
|
||||||
|
if self.textureCache == nil {
|
||||||
|
self.setupTextureCache()
|
||||||
|
}
|
||||||
|
var cvTextureOut: CVMetalTexture?
|
||||||
|
CVMetalTextureCacheCreateTextureFromImage(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
textureCache!,
|
||||||
|
previewPixelBuffer,
|
||||||
|
nil,
|
||||||
|
.bgra8Unorm,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0,
|
||||||
|
&cvTextureOut)
|
||||||
|
guard let cvTexture = cvTextureOut, let texture = CVMetalTextureGetTexture(cvTexture) else {
|
||||||
|
CVMetalTextureCacheFlush(self.textureCache!, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if texture.width != self.textureWidth ||
|
||||||
|
texture.height != self.textureHeight ||
|
||||||
|
self.bounds != self._bounds ||
|
||||||
|
rotation != self.textureRotation ||
|
||||||
|
mirroring != self.textureMirroring {
|
||||||
|
self.setupTransform(width: texture.width, height: texture.height, rotation: rotation, mirroring: mirroring)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||||
|
CVMetalTextureCacheFlush(self.textureCache!, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor) else {
|
||||||
|
CVMetalTextureCacheFlush(self.textureCache!, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commandEncoder.setRenderPipelineState(self.renderPipelineState!)
|
||||||
|
commandEncoder.setVertexBuffer(self.vertexCoordBuffer, offset: 0, index: 0)
|
||||||
|
commandEncoder.setVertexBuffer(self.texCoordBuffer, offset: 0, index: 1)
|
||||||
|
commandEncoder.setFragmentTexture(texture, index: 0)
|
||||||
|
commandEncoder.setFragmentSamplerState(self.sampler, index: 0)
|
||||||
|
commandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||||
|
commandEncoder.endEncoding()
|
||||||
|
|
||||||
|
commandBuffer.present(drawable)
|
||||||
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var captureDeviceResolution: CGSize = CGSize() {
|
||||||
|
didSet {
|
||||||
|
if oldValue.width.isZero, !self.captureDeviceResolution.width.isZero {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
self.setupVisionDrawingLayers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var detectionOverlayLayer: CALayer?
|
||||||
|
var detectedFaceRectangleShapeLayer: CAShapeLayer?
|
||||||
|
var detectedFaceLandmarksShapeLayer: CAShapeLayer?
|
||||||
|
|
||||||
|
func drawFaceObservations(_ faceObservations: [VNFaceObservation]) {
|
||||||
|
guard let faceRectangleShapeLayer = self.detectedFaceRectangleShapeLayer,
|
||||||
|
let faceLandmarksShapeLayer = self.detectedFaceLandmarksShapeLayer
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
|
||||||
|
CATransaction.setValue(NSNumber(value: true), forKey: kCATransactionDisableActions)
|
||||||
|
|
||||||
|
self.detectionOverlayLayer?.isHidden = faceObservations.isEmpty
|
||||||
|
|
||||||
|
let faceRectanglePath = CGMutablePath()
|
||||||
|
let faceLandmarksPath = CGMutablePath()
|
||||||
|
|
||||||
|
for faceObservation in faceObservations {
|
||||||
|
self.addIndicators(to: faceRectanglePath,
|
||||||
|
faceLandmarksPath: faceLandmarksPath,
|
||||||
|
for: faceObservation)
|
||||||
|
}
|
||||||
|
|
||||||
|
faceRectangleShapeLayer.path = faceRectanglePath
|
||||||
|
faceLandmarksShapeLayer.path = faceLandmarksPath
|
||||||
|
|
||||||
|
self.updateLayerGeometry()
|
||||||
|
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func addPoints(in landmarkRegion: VNFaceLandmarkRegion2D, to path: CGMutablePath, applying affineTransform: CGAffineTransform, closingWhenComplete closePath: Bool) {
|
||||||
|
let pointCount = landmarkRegion.pointCount
|
||||||
|
if pointCount > 1 {
|
||||||
|
let points: [CGPoint] = landmarkRegion.normalizedPoints
|
||||||
|
path.move(to: points[0], transform: affineTransform)
|
||||||
|
path.addLines(between: points, transform: affineTransform)
|
||||||
|
if closePath {
|
||||||
|
path.addLine(to: points[0], transform: affineTransform)
|
||||||
|
path.closeSubpath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func addIndicators(to faceRectanglePath: CGMutablePath, faceLandmarksPath: CGMutablePath, for faceObservation: VNFaceObservation) {
|
||||||
|
let displaySize = self.captureDeviceResolution
|
||||||
|
|
||||||
|
let faceBounds = VNImageRectForNormalizedRect(faceObservation.boundingBox, Int(displaySize.width), Int(displaySize.height))
|
||||||
|
faceRectanglePath.addRect(faceBounds)
|
||||||
|
|
||||||
|
if let landmarks = faceObservation.landmarks {
|
||||||
|
let affineTransform = CGAffineTransform(translationX: faceBounds.origin.x, y: faceBounds.origin.y)
|
||||||
|
.scaledBy(x: faceBounds.size.width, y: faceBounds.size.height)
|
||||||
|
|
||||||
|
let openLandmarkRegions: [VNFaceLandmarkRegion2D?] = [
|
||||||
|
landmarks.leftEyebrow,
|
||||||
|
landmarks.rightEyebrow,
|
||||||
|
landmarks.faceContour,
|
||||||
|
landmarks.noseCrest,
|
||||||
|
landmarks.medianLine
|
||||||
|
]
|
||||||
|
for openLandmarkRegion in openLandmarkRegions where openLandmarkRegion != nil {
|
||||||
|
self.addPoints(in: openLandmarkRegion!, to: faceLandmarksPath, applying: affineTransform, closingWhenComplete: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let closedLandmarkRegions: [VNFaceLandmarkRegion2D?] = [
|
||||||
|
landmarks.leftEye,
|
||||||
|
landmarks.rightEye,
|
||||||
|
landmarks.outerLips,
|
||||||
|
landmarks.innerLips,
|
||||||
|
landmarks.nose
|
||||||
|
]
|
||||||
|
for closedLandmarkRegion in closedLandmarkRegions where closedLandmarkRegion != nil {
|
||||||
|
self.addPoints(in: closedLandmarkRegion!, to: faceLandmarksPath, applying: affineTransform, closingWhenComplete: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func radiansForDegrees(_ degrees: CGFloat) -> CGFloat {
|
||||||
|
return CGFloat(Double(degrees) * Double.pi / 180.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func updateLayerGeometry() {
|
||||||
|
guard let overlayLayer = self.detectionOverlayLayer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CATransaction.setValue(NSNumber(value: true), forKey: kCATransactionDisableActions)
|
||||||
|
|
||||||
|
let videoPreviewRect = self.bounds
|
||||||
|
|
||||||
|
var rotation: CGFloat
|
||||||
|
var scaleX: CGFloat
|
||||||
|
var scaleY: CGFloat
|
||||||
|
|
||||||
|
// Rotate the layer into screen orientation.
|
||||||
|
switch UIDevice.current.orientation {
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
rotation = 180
|
||||||
|
scaleX = videoPreviewRect.width / captureDeviceResolution.width
|
||||||
|
scaleY = videoPreviewRect.height / captureDeviceResolution.height
|
||||||
|
|
||||||
|
case .landscapeLeft:
|
||||||
|
rotation = 90
|
||||||
|
scaleX = videoPreviewRect.height / captureDeviceResolution.width
|
||||||
|
scaleY = scaleX
|
||||||
|
|
||||||
|
case .landscapeRight:
|
||||||
|
rotation = -90
|
||||||
|
scaleX = videoPreviewRect.height / captureDeviceResolution.width
|
||||||
|
scaleY = scaleX
|
||||||
|
|
||||||
|
default:
|
||||||
|
rotation = 0
|
||||||
|
scaleX = videoPreviewRect.width / captureDeviceResolution.width
|
||||||
|
scaleY = videoPreviewRect.height / captureDeviceResolution.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale and mirror the image to ensure upright presentation.
|
||||||
|
let affineTransform = CGAffineTransform(rotationAngle: radiansForDegrees(rotation))
|
||||||
|
.scaledBy(x: scaleX, y: -scaleY)
|
||||||
|
overlayLayer.setAffineTransform(affineTransform)
|
||||||
|
|
||||||
|
// Cover entire screen UI.
|
||||||
|
let rootLayerBounds = self.bounds
|
||||||
|
overlayLayer.position = CGPoint(x: rootLayerBounds.midX, y: rootLayerBounds.midY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func setupVisionDrawingLayers() {
|
||||||
|
let captureDeviceResolution = self.captureDeviceResolution
|
||||||
|
let rootLayer = self.layer
|
||||||
|
|
||||||
|
let captureDeviceBounds = CGRect(x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: captureDeviceResolution.width,
|
||||||
|
height: captureDeviceResolution.height)
|
||||||
|
|
||||||
|
let captureDeviceBoundsCenterPoint = CGPoint(x: captureDeviceBounds.midX,
|
||||||
|
y: captureDeviceBounds.midY)
|
||||||
|
|
||||||
|
let normalizedCenterPoint = CGPoint(x: 0.5, y: 0.5)
|
||||||
|
|
||||||
|
let overlayLayer = CALayer()
|
||||||
|
overlayLayer.name = "DetectionOverlay"
|
||||||
|
overlayLayer.masksToBounds = true
|
||||||
|
overlayLayer.anchorPoint = normalizedCenterPoint
|
||||||
|
overlayLayer.bounds = captureDeviceBounds
|
||||||
|
overlayLayer.position = CGPoint(x: rootLayer.bounds.midX, y: rootLayer.bounds.midY)
|
||||||
|
|
||||||
|
let faceRectangleShapeLayer = CAShapeLayer()
|
||||||
|
faceRectangleShapeLayer.name = "RectangleOutlineLayer"
|
||||||
|
faceRectangleShapeLayer.bounds = captureDeviceBounds
|
||||||
|
faceRectangleShapeLayer.anchorPoint = normalizedCenterPoint
|
||||||
|
faceRectangleShapeLayer.position = captureDeviceBoundsCenterPoint
|
||||||
|
faceRectangleShapeLayer.fillColor = nil
|
||||||
|
faceRectangleShapeLayer.strokeColor = UIColor.green.withAlphaComponent(0.2).cgColor
|
||||||
|
faceRectangleShapeLayer.lineWidth = 2
|
||||||
|
|
||||||
|
let faceLandmarksShapeLayer = CAShapeLayer()
|
||||||
|
faceLandmarksShapeLayer.name = "FaceLandmarksLayer"
|
||||||
|
faceLandmarksShapeLayer.bounds = captureDeviceBounds
|
||||||
|
faceLandmarksShapeLayer.anchorPoint = normalizedCenterPoint
|
||||||
|
faceLandmarksShapeLayer.position = captureDeviceBoundsCenterPoint
|
||||||
|
faceLandmarksShapeLayer.fillColor = nil
|
||||||
|
faceLandmarksShapeLayer.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor
|
||||||
|
faceLandmarksShapeLayer.lineWidth = 2
|
||||||
|
faceLandmarksShapeLayer.shadowOpacity = 0.7
|
||||||
|
faceLandmarksShapeLayer.shadowRadius = 2
|
||||||
|
|
||||||
|
overlayLayer.addSublayer(faceRectangleShapeLayer)
|
||||||
|
faceRectangleShapeLayer.addSublayer(faceLandmarksShapeLayer)
|
||||||
|
self.layer.addSublayer(overlayLayer)
|
||||||
|
|
||||||
|
self.detectionOverlayLayer = overlayLayer
|
||||||
|
self.detectedFaceRectangleShapeLayer = faceRectangleShapeLayer
|
||||||
|
self.detectedFaceLandmarksShapeLayer = faceLandmarksShapeLayer
|
||||||
|
|
||||||
|
self.updateLayerGeometry()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,8 @@
|
|||||||
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
import Accelerate
|
||||||
|
import CoreImage
|
||||||
|
|
||||||
extension AVFrameRateRange {
|
extension AVFrameRateRange {
|
||||||
func clamp(rate: Float64) -> Float64 {
|
func clamp(rate: Float64) -> Float64 {
|
||||||
@ -31,7 +35,6 @@ extension AVCaptureDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let diff = frameRates.map { abs($0 - fps) }
|
let diff = frameRates.map { abs($0 - fps) }
|
||||||
|
|
||||||
if let minElement: Float64 = diff.min() {
|
if let minElement: Float64 = diff.min() {
|
||||||
for i in 0..<diff.count where diff[i] == minElement {
|
for i in 0..<diff.count where diff[i] == minElement {
|
||||||
return (frameRates[i], durations[i])
|
return (frameRates[i], durations[i])
|
||||||
@ -40,4 +43,315 @@ extension AVCaptureDevice {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var neutralZoomFactor: CGFloat {
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
if let indexOfWideAngle = self.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }), indexOfWideAngle > 0 {
|
||||||
|
let zoomFactor = self.virtualDeviceSwitchOverVideoZoomFactors[indexOfWideAngle - 1]
|
||||||
|
return CGFloat(zoomFactor.doubleValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CMSampleBuffer {
|
||||||
|
var presentationTimestamp: CMTime {
|
||||||
|
return CMSampleBufferGetPresentationTimeStamp(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var type: CMMediaType {
|
||||||
|
if let formatDescription = CMSampleBufferGetFormatDescription(self) {
|
||||||
|
return CMFormatDescriptionGetMediaType(formatDescription)
|
||||||
|
} else {
|
||||||
|
return kCMMediaType_Video
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AVCaptureVideoOrientation {
|
||||||
|
init?(interfaceOrientation: UIInterfaceOrientation) {
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .portrait: self = .portrait
|
||||||
|
case .portraitUpsideDown: self = .portraitUpsideDown
|
||||||
|
case .landscapeLeft: self = .landscapeLeft
|
||||||
|
case .landscapeRight: self = .landscapeRight
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CameraPreviewView.Rotation {
|
||||||
|
init?(with interfaceOrientation: UIInterfaceOrientation, videoOrientation: AVCaptureVideoOrientation, cameraPosition: AVCaptureDevice.Position) {
|
||||||
|
switch videoOrientation {
|
||||||
|
case .portrait:
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .landscapeRight:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .landscapeLeft:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .portrait:
|
||||||
|
self = .rotate0Degrees
|
||||||
|
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
self = .rotate180Degrees
|
||||||
|
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .landscapeRight:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .landscapeLeft:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .portrait:
|
||||||
|
self = .rotate180Degrees
|
||||||
|
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
self = .rotate0Degrees
|
||||||
|
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case .landscapeRight:
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .landscapeRight:
|
||||||
|
self = .rotate0Degrees
|
||||||
|
|
||||||
|
case .landscapeLeft:
|
||||||
|
self = .rotate180Degrees
|
||||||
|
|
||||||
|
case .portrait:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case .landscapeLeft:
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .landscapeLeft:
|
||||||
|
self = .rotate0Degrees
|
||||||
|
|
||||||
|
case .landscapeRight:
|
||||||
|
self = .rotate180Degrees
|
||||||
|
|
||||||
|
case .portrait:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
if cameraPosition == .front {
|
||||||
|
self = .rotate270Degrees
|
||||||
|
} else {
|
||||||
|
self = .rotate90Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
fatalError("Unknown orientation.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exifOrientationForDeviceOrientation(_ deviceOrientation: UIDeviceOrientation) -> CGImagePropertyOrientation {
|
||||||
|
switch deviceOrientation {
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
return .rightMirrored
|
||||||
|
case .landscapeLeft:
|
||||||
|
return .downMirrored
|
||||||
|
case .landscapeRight:
|
||||||
|
return .upMirrored
|
||||||
|
default:
|
||||||
|
return .leftMirrored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
|
||||||
|
to dstPixelBuffer: CVPixelBuffer,
|
||||||
|
cropX: Int,
|
||||||
|
cropY: Int,
|
||||||
|
cropWidth: Int,
|
||||||
|
cropHeight: Int,
|
||||||
|
scaleWidth: Int,
|
||||||
|
scaleHeight: Int) {
|
||||||
|
|
||||||
|
assert(CVPixelBufferGetWidth(dstPixelBuffer) >= scaleWidth)
|
||||||
|
assert(CVPixelBufferGetHeight(dstPixelBuffer) >= scaleHeight)
|
||||||
|
|
||||||
|
let srcFlags = CVPixelBufferLockFlags.readOnly
|
||||||
|
let dstFlags = CVPixelBufferLockFlags(rawValue: 0)
|
||||||
|
|
||||||
|
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(srcPixelBuffer, srcFlags) else {
|
||||||
|
print("Error: could not lock source pixel buffer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, srcFlags) }
|
||||||
|
|
||||||
|
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(dstPixelBuffer, dstFlags) else {
|
||||||
|
print("Error: could not lock destination pixel buffer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { CVPixelBufferUnlockBaseAddress(dstPixelBuffer, dstFlags) }
|
||||||
|
|
||||||
|
guard let srcData = CVPixelBufferGetBaseAddress(srcPixelBuffer),
|
||||||
|
let dstData = CVPixelBufferGetBaseAddress(dstPixelBuffer) else {
|
||||||
|
print("Error: could not get pixel buffer base address")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let srcBytesPerRow = CVPixelBufferGetBytesPerRow(srcPixelBuffer)
|
||||||
|
let offset = cropY*srcBytesPerRow + cropX*4
|
||||||
|
var srcBuffer = vImage_Buffer(data: srcData.advanced(by: offset),
|
||||||
|
height: vImagePixelCount(cropHeight),
|
||||||
|
width: vImagePixelCount(cropWidth),
|
||||||
|
rowBytes: srcBytesPerRow)
|
||||||
|
|
||||||
|
let dstBytesPerRow = CVPixelBufferGetBytesPerRow(dstPixelBuffer)
|
||||||
|
var dstBuffer = vImage_Buffer(data: dstData,
|
||||||
|
height: vImagePixelCount(scaleHeight),
|
||||||
|
width: vImagePixelCount(scaleWidth),
|
||||||
|
rowBytes: dstBytesPerRow)
|
||||||
|
|
||||||
|
let error = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(0))
|
||||||
|
if error != kvImageNoError {
|
||||||
|
print("Error:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
|
||||||
|
to dstPixelBuffer: CVPixelBuffer,
|
||||||
|
width: Int, height: Int) {
|
||||||
|
resizePixelBuffer(from: srcPixelBuffer, to: dstPixelBuffer,
|
||||||
|
cropX: 0, cropY: 0,
|
||||||
|
cropWidth: CVPixelBufferGetWidth(srcPixelBuffer),
|
||||||
|
cropHeight: CVPixelBufferGetHeight(srcPixelBuffer),
|
||||||
|
scaleWidth: width, scaleHeight: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizePixelBuffer(_ pixelBuffer: CVPixelBuffer,
|
||||||
|
width: Int, height: Int,
|
||||||
|
output: CVPixelBuffer, context: CIContext) {
|
||||||
|
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||||
|
let sx = CGFloat(width) / CGFloat(CVPixelBufferGetWidth(pixelBuffer))
|
||||||
|
let sy = CGFloat(height) / CGFloat(CVPixelBufferGetHeight(pixelBuffer))
|
||||||
|
let scaleTransform = CGAffineTransform(scaleX: sx, y: sy)
|
||||||
|
let scaledImage = ciImage.transformed(by: scaleTransform)
|
||||||
|
context.render(scaledImage, to: output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageFromCVPixelBuffer(_ pixelBuffer: CVPixelBuffer, orientation: UIImage.Orientation) -> UIImage? {
|
||||||
|
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
|
||||||
|
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||||
|
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||||
|
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
||||||
|
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||||
|
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
|
||||||
|
guard let context = CGContext(
|
||||||
|
data: baseAddress,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
space: colorSpace,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
|
||||||
|
) else {
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cgImage = context.makeImage() else {
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
|
||||||
|
return UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CVPixelBuffer {
|
||||||
|
func deepCopy() -> CVPixelBuffer? {
|
||||||
|
let width = CVPixelBufferGetWidth(self)
|
||||||
|
let height = CVPixelBufferGetHeight(self)
|
||||||
|
let format = CVPixelBufferGetPixelFormatType(self)
|
||||||
|
|
||||||
|
let attributes: [NSObject: AnyObject] = [
|
||||||
|
kCVPixelBufferCGImageCompatibilityKey: true as AnyObject,
|
||||||
|
kCVPixelBufferCGBitmapContextCompatibilityKey: true as AnyObject
|
||||||
|
]
|
||||||
|
|
||||||
|
var newPixelBuffer: CVPixelBuffer?
|
||||||
|
let status = CVPixelBufferCreate(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
format,
|
||||||
|
attributes as CFDictionary,
|
||||||
|
&newPixelBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
guard status == kCVReturnSuccess, let unwrappedPixelBuffer = newPixelBuffer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
CVPixelBufferLockBaseAddress(self, .readOnly)
|
||||||
|
CVPixelBufferLockBaseAddress(unwrappedPixelBuffer, [])
|
||||||
|
|
||||||
|
guard let sourceBaseAddress = CVPixelBufferGetBaseAddress(self),
|
||||||
|
let destinationBaseAddress = CVPixelBufferGetBaseAddress(unwrappedPixelBuffer) else {
|
||||||
|
CVPixelBufferUnlockBaseAddress(self, .readOnly)
|
||||||
|
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceBytesPerRow = CVPixelBufferGetBytesPerRow(self)
|
||||||
|
let destinationBytesPerRow = CVPixelBufferGetBytesPerRow(unwrappedPixelBuffer)
|
||||||
|
|
||||||
|
let imageSize = height * min(sourceBytesPerRow, destinationBytesPerRow)
|
||||||
|
|
||||||
|
memcpy(destinationBaseAddress, sourceBaseAddress, imageSize)
|
||||||
|
|
||||||
|
CVPixelBufferUnlockBaseAddress(self, .readOnly)
|
||||||
|
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
|
||||||
|
|
||||||
|
return unwrappedPixelBuffer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
110
submodules/Camera/Sources/PhotoCaptureContext.swift
Normal file
110
submodules/Camera/Sources/PhotoCaptureContext.swift
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
public enum PhotoCaptureResult: Equatable {
|
||||||
|
case began
|
||||||
|
case finished(UIImage, UIImage?, Double)
|
||||||
|
case failed
|
||||||
|
|
||||||
|
public static func == (lhs: PhotoCaptureResult, rhs: PhotoCaptureResult) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case .began:
|
||||||
|
if case .began = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .failed:
|
||||||
|
if case .failed = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .finished(_, _, lhsTime):
|
||||||
|
if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate {
|
||||||
|
private let pipe = ValuePipe<PhotoCaptureResult>()
|
||||||
|
private let orientation: AVCaptureVideoOrientation
|
||||||
|
private let mirror: Bool
|
||||||
|
|
||||||
|
init(settings: AVCapturePhotoSettings, orientation: AVCaptureVideoOrientation, mirror: Bool) {
|
||||||
|
self.orientation = orientation
|
||||||
|
self.mirror = mirror
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||||
|
self.pipe.putNext(.began)
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
||||||
|
if let _ = error {
|
||||||
|
self.pipe.putNext(.failed)
|
||||||
|
} else {
|
||||||
|
guard let photoPixelBuffer = photo.pixelBuffer else {
|
||||||
|
print("Error occurred while capturing photo: Missing pixel buffer (\(String(describing: error)))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoFormatDescription: CMFormatDescription?
|
||||||
|
CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: photoPixelBuffer, formatDescriptionOut: &photoFormatDescription)
|
||||||
|
|
||||||
|
var orientation: UIImage.Orientation = .right
|
||||||
|
if self.orientation == .landscapeLeft {
|
||||||
|
orientation = .down
|
||||||
|
} else if self.orientation == .landscapeRight {
|
||||||
|
orientation = .up
|
||||||
|
} else if self.orientation == .portraitUpsideDown {
|
||||||
|
orientation = .left
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalPixelBuffer = photoPixelBuffer
|
||||||
|
let ciContext = CIContext()
|
||||||
|
let renderedCIImage = CIImage(cvImageBuffer: finalPixelBuffer)
|
||||||
|
if let cgImage = ciContext.createCGImage(renderedCIImage, from: renderedCIImage.extent) {
|
||||||
|
var image = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
|
||||||
|
if image.imageOrientation != .up {
|
||||||
|
UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)
|
||||||
|
if self.mirror, let context = UIGraphicsGetCurrentContext() {
|
||||||
|
context.translateBy(x: image.size.width / 2.0, y: image.size.height / 2.0)
|
||||||
|
context.scaleBy(x: -1.0, y: 1.0)
|
||||||
|
context.translateBy(x: -image.size.width / 2.0, y: -image.size.height / 2.0)
|
||||||
|
}
|
||||||
|
image.draw(in: CGRect(origin: .zero, size: image.size))
|
||||||
|
if let currentImage = UIGraphicsGetImageFromCurrentImageContext() {
|
||||||
|
image = currentImage
|
||||||
|
}
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
}
|
||||||
|
self.pipe.putNext(.finished(image, nil, CACurrentMediaTime()))
|
||||||
|
} else {
|
||||||
|
self.pipe.putNext(.failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var signal: Signal<PhotoCaptureResult, NoError> {
|
||||||
|
return self.pipe.signal()
|
||||||
|
|> take(until: { next in
|
||||||
|
let complete: Bool
|
||||||
|
switch next {
|
||||||
|
case .finished, .failed:
|
||||||
|
complete = true
|
||||||
|
default:
|
||||||
|
complete = false
|
||||||
|
}
|
||||||
|
return SignalTakeAction(passthrough: true, complete: complete)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
535
submodules/Camera/Sources/VideoRecorder.swift
Normal file
535
submodules/Camera/Sources/VideoRecorder.swift
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
import CoreImage
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
private extension CMSampleBuffer {
|
||||||
|
var endTime: CMTime {
|
||||||
|
let presentationTime = CMSampleBufferGetPresentationTimeStamp(self)
|
||||||
|
let duration = CMSampleBufferGetDuration(self)
|
||||||
|
return presentationTime + duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VideoRecorderImpl {
|
||||||
|
public enum RecorderError: LocalizedError {
|
||||||
|
case generic
|
||||||
|
case avError(Error)
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .generic:
|
||||||
|
return "Error"
|
||||||
|
case let .avError(error):
|
||||||
|
return error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue = DispatchQueue(label: "VideoRecorder")
|
||||||
|
|
||||||
|
private var assetWriter: AVAssetWriter
|
||||||
|
private var videoInput: AVAssetWriterInput?
|
||||||
|
private var audioInput: AVAssetWriterInput?
|
||||||
|
|
||||||
|
private let imageContext = CIContext()
|
||||||
|
private var transitionImage: UIImage?
|
||||||
|
private var savedTransitionImage = false
|
||||||
|
|
||||||
|
private var pendingAudioSampleBuffers: [CMSampleBuffer] = []
|
||||||
|
|
||||||
|
private var _duration: CMTime = .zero
|
||||||
|
public var duration: CMTime {
|
||||||
|
self.queue.sync { _duration }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastVideoSampleTime: CMTime = .invalid
|
||||||
|
private var recordingStartSampleTime: CMTime = .invalid
|
||||||
|
private var recordingStopSampleTime: CMTime = .invalid
|
||||||
|
|
||||||
|
private var positionChangeTimestamps: [(Camera.Position, CMTime)] = []
|
||||||
|
|
||||||
|
private let configuration: VideoRecorder.Configuration
|
||||||
|
private let orientation: AVCaptureVideoOrientation
|
||||||
|
private let videoTransform: CGAffineTransform
|
||||||
|
private let url: URL
|
||||||
|
fileprivate var completion: (Bool, UIImage?, [(Camera.Position, CMTime)]?) -> Void = { _, _, _ in }
|
||||||
|
|
||||||
|
private let error = Atomic<Error?>(value: nil)
|
||||||
|
|
||||||
|
private var stopped = false
|
||||||
|
private var hasAllVideoBuffers = false
|
||||||
|
private var hasAllAudioBuffers = false
|
||||||
|
|
||||||
|
public init?(configuration: VideoRecorder.Configuration, orientation: AVCaptureVideoOrientation, fileUrl: URL) {
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
|
var transform: CGAffineTransform = CGAffineTransform(rotationAngle: .pi / 2.0)
|
||||||
|
if orientation == .landscapeLeft {
|
||||||
|
transform = CGAffineTransform(rotationAngle: .pi)
|
||||||
|
} else if orientation == .landscapeRight {
|
||||||
|
transform = CGAffineTransform(rotationAngle: 0.0)
|
||||||
|
} else if orientation == .portraitUpsideDown {
|
||||||
|
transform = CGAffineTransform(rotationAngle: -.pi / 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.orientation = orientation
|
||||||
|
self.videoTransform = transform
|
||||||
|
self.url = fileUrl
|
||||||
|
|
||||||
|
try? FileManager.default.removeItem(at: url)
|
||||||
|
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.assetWriter = assetWriter
|
||||||
|
self.assetWriter.shouldOptimizeForNetworkUse = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hasError() -> Error? {
|
||||||
|
return self.error.with { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func start() {
|
||||||
|
self.queue.async {
|
||||||
|
self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func markPositionChange(position: Camera.Position, time: CMTime? = nil) {
|
||||||
|
self.queue.async {
|
||||||
|
guard self.recordingStartSampleTime.isValid || time != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let time {
|
||||||
|
self.positionChangeTimestamps.append((position, time))
|
||||||
|
} else {
|
||||||
|
let currentTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||||
|
let delta = currentTime - self.recordingStartSampleTime
|
||||||
|
self.positionChangeTimestamps.append((position, delta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||||
|
if let _ = self.hasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Video else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||||
|
self.queue.async {
|
||||||
|
guard !self.stopped && self.error.with({ $0 }) == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var failed = false
|
||||||
|
if self.videoInput == nil {
|
||||||
|
let videoSettings = self.configuration.videoSettings
|
||||||
|
if self.assetWriter.canApply(outputSettings: videoSettings, forMediaType: .video) {
|
||||||
|
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings, sourceFormatHint: formatDescription)
|
||||||
|
videoInput.expectsMediaDataInRealTime = true
|
||||||
|
videoInput.transform = self.videoTransform
|
||||||
|
if self.assetWriter.canAdd(videoInput) {
|
||||||
|
self.assetWriter.add(videoInput)
|
||||||
|
self.videoInput = videoInput
|
||||||
|
} else {
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
print("error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.assetWriter.status == .unknown {
|
||||||
|
if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !self.assetWriter.startWriting() {
|
||||||
|
if let error = self.assetWriter.error {
|
||||||
|
self.transitionToFailedStatus(error: .avError(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assetWriter.startSession(atSourceTime: presentationTime)
|
||||||
|
self.recordingStartSampleTime = presentationTime
|
||||||
|
self.lastVideoSampleTime = presentationTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.assetWriter.status == .writing {
|
||||||
|
if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime {
|
||||||
|
self.hasAllVideoBuffers = true
|
||||||
|
self.maybeFinish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData {
|
||||||
|
if videoInput.append(sampleBuffer) {
|
||||||
|
self.lastVideoSampleTime = presentationTime
|
||||||
|
let startTime = self.recordingStartSampleTime
|
||||||
|
let duration = presentationTime - startTime
|
||||||
|
self._duration = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||||
|
self.savedTransitionImage = true
|
||||||
|
Queue.concurrentBackgroundQueue().async {
|
||||||
|
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||||
|
if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||||
|
var orientation: UIImage.Orientation = .right
|
||||||
|
if self.orientation == .landscapeLeft {
|
||||||
|
orientation = .down
|
||||||
|
} else if self.orientation == .landscapeRight {
|
||||||
|
orientation = .up
|
||||||
|
} else if self.orientation == .portraitUpsideDown {
|
||||||
|
orientation = .left
|
||||||
|
}
|
||||||
|
self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
|
||||||
|
} else {
|
||||||
|
self.savedTransitionImage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.tryAppendingPendingAudioBuffers() {
|
||||||
|
self.transitionToFailedStatus(error: .generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||||
|
if let _ = self.hasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Audio else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.async {
|
||||||
|
guard !self.stopped && self.error.with({ $0 }) == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var failed = false
|
||||||
|
if self.audioInput == nil {
|
||||||
|
var audioSettings = self.configuration.audioSettings
|
||||||
|
if let currentAudioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) {
|
||||||
|
audioSettings[AVSampleRateKey] = currentAudioStreamBasicDescription.pointee.mSampleRate
|
||||||
|
audioSettings[AVNumberOfChannelsKey] = currentAudioStreamBasicDescription.pointee.mChannelsPerFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
var audioChannelLayoutSize: Int = 0
|
||||||
|
let currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(formatDescription, sizeOut: &audioChannelLayoutSize)
|
||||||
|
let currentChannelLayoutData: Data
|
||||||
|
if let currentChannelLayout = currentChannelLayout, audioChannelLayoutSize > 0 {
|
||||||
|
currentChannelLayoutData = Data(bytes: currentChannelLayout, count: audioChannelLayoutSize)
|
||||||
|
} else {
|
||||||
|
currentChannelLayoutData = Data()
|
||||||
|
}
|
||||||
|
audioSettings[AVChannelLayoutKey] = currentChannelLayoutData
|
||||||
|
|
||||||
|
if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) {
|
||||||
|
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings, sourceFormatHint: formatDescription)
|
||||||
|
audioInput.expectsMediaDataInRealTime = true
|
||||||
|
if self.assetWriter.canAdd(audioInput) {
|
||||||
|
self.assetWriter.add(audioInput)
|
||||||
|
self.audioInput = audioInput
|
||||||
|
} else {
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
print("error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.assetWriter.status == .writing {
|
||||||
|
if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime {
|
||||||
|
self.hasAllAudioBuffers = true
|
||||||
|
self.maybeFinish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result = false
|
||||||
|
if self.tryAppendingPendingAudioBuffers() {
|
||||||
|
if self.tryAppendingAudioSampleBuffer(sampleBuffer) {
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !result {
|
||||||
|
self.transitionToFailedStatus(error: .generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cancelRecording(completion: @escaping () -> Void) {
|
||||||
|
self.queue.async {
|
||||||
|
if self.stopped {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.stopped = true
|
||||||
|
self.pendingAudioSampleBuffers = []
|
||||||
|
if self.assetWriter.status == .writing {
|
||||||
|
self.assetWriter.cancelWriting()
|
||||||
|
}
|
||||||
|
let fileManager = FileManager()
|
||||||
|
try? fileManager.removeItem(at: self.url)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isRecording: Bool {
|
||||||
|
self.queue.sync { !(self.hasAllVideoBuffers && self.hasAllAudioBuffers) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopRecording() {
|
||||||
|
self.queue.async {
|
||||||
|
var stopTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||||
|
if self.recordingStartSampleTime.isValid {
|
||||||
|
if (stopTime - self.recordingStartSampleTime).seconds < 1.0 {
|
||||||
|
stopTime = self.recordingStartSampleTime + CMTime(seconds: 1.0, preferredTimescale: self.recordingStartSampleTime.timescale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recordingStopSampleTime = stopTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func maybeFinish() {
|
||||||
|
self.queue.async {
|
||||||
|
guard self.hasAllVideoBuffers && self.hasAllVideoBuffers else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.stopped = true
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func finish() {
|
||||||
|
self.queue.async {
|
||||||
|
let completion = self.completion
|
||||||
|
if self.recordingStopSampleTime == .invalid {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = self.error.with({ $0 }) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.tryAppendingPendingAudioBuffers() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.assetWriter.status == .writing {
|
||||||
|
self.assetWriter.finishWriting {
|
||||||
|
if let _ = self.assetWriter.error {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(true, self.transitionImage, self.positionChangeTimestamps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let _ = self.assetWriter.error {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tryAppendingPendingAudioBuffers() -> Bool {
|
||||||
|
dispatchPrecondition(condition: .onQueue(self.queue))
|
||||||
|
guard self.pendingAudioSampleBuffers.count > 0 else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = true
|
||||||
|
let (sampleBuffersToAppend, pendingSampleBuffers) = self.pendingAudioSampleBuffers.stableGroup(using: { $0.endTime <= self.lastVideoSampleTime })
|
||||||
|
for sampleBuffer in sampleBuffersToAppend {
|
||||||
|
if !self.internalAppendAudioSampleBuffer(sampleBuffer) {
|
||||||
|
result = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.pendingAudioSampleBuffers = pendingSampleBuffers
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tryAppendingAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
||||||
|
dispatchPrecondition(condition: .onQueue(self.queue))
|
||||||
|
|
||||||
|
var result = true
|
||||||
|
if sampleBuffer.endTime > self.lastVideoSampleTime {
|
||||||
|
self.pendingAudioSampleBuffers.append(sampleBuffer)
|
||||||
|
} else {
|
||||||
|
result = self.internalAppendAudioSampleBuffer(sampleBuffer)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func internalAppendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
||||||
|
if let audioInput = self.audioInput, audioInput.isReadyForMoreMediaData {
|
||||||
|
if !audioInput.append(sampleBuffer) {
|
||||||
|
if let _ = self.assetWriter.error {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func transitionToFailedStatus(error: RecorderError) {
|
||||||
|
let _ = self.error.modify({ _ in return error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Sequence {
|
||||||
|
func stableGroup(using predicate: (Element) throws -> Bool) rethrows -> ([Element], [Element]) {
|
||||||
|
var trueGroup: [Element] = []
|
||||||
|
var falseGroup: [Element] = []
|
||||||
|
for element in self {
|
||||||
|
if try predicate(element) {
|
||||||
|
trueGroup.append(element)
|
||||||
|
} else {
|
||||||
|
falseGroup.append(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (trueGroup, falseGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class VideoRecorder {
|
||||||
|
var duration: Double? {
|
||||||
|
return self.impl.duration.seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Result {
|
||||||
|
enum Error {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
case success(UIImage?, Double, [(Camera.Position, Double)])
|
||||||
|
case initError(Error)
|
||||||
|
case writeError(Error)
|
||||||
|
case finishError(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Configuration {
|
||||||
|
var videoSettings: [String: Any]
|
||||||
|
var audioSettings: [String: Any]
|
||||||
|
|
||||||
|
init(videoSettings: [String: Any], audioSettings: [String: Any]) {
|
||||||
|
self.videoSettings = videoSettings
|
||||||
|
self.audioSettings = audioSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAudio: Bool {
|
||||||
|
return !self.audioSettings.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let impl: VideoRecorderImpl
|
||||||
|
fileprivate let configuration: Configuration
|
||||||
|
fileprivate let fileUrl: URL
|
||||||
|
private let completion: (Result) -> Void
|
||||||
|
|
||||||
|
public var isRecording: Bool {
|
||||||
|
return self.impl.isRecording
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(configuration: Configuration, orientation: AVCaptureVideoOrientation, fileUrl: URL, completion: @escaping (Result) -> Void) {
|
||||||
|
self.configuration = configuration
|
||||||
|
self.fileUrl = fileUrl
|
||||||
|
self.completion = completion
|
||||||
|
|
||||||
|
guard let impl = VideoRecorderImpl(configuration: configuration, orientation: orientation, fileUrl: fileUrl) else {
|
||||||
|
completion(.initError(.generic))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.impl = impl
|
||||||
|
impl.completion = { [weak self] result, transitionImage, positionChangeTimestamps in
|
||||||
|
if let self {
|
||||||
|
let duration = self.duration ?? 0.0
|
||||||
|
if result {
|
||||||
|
var timestamps: [(Camera.Position, Double)] = []
|
||||||
|
if let positionChangeTimestamps {
|
||||||
|
for (position, time) in positionChangeTimestamps {
|
||||||
|
timestamps.append((position, time.seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.completion(.success(transitionImage, duration, timestamps))
|
||||||
|
} else {
|
||||||
|
self.completion(.finishError(.generic))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
self.impl.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
self.impl.stopRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markPositionChange(position: Camera.Position, time: CMTime? = nil) {
|
||||||
|
self.impl.markPositionChange(position: position, time: time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||||
|
guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let type = CMFormatDescriptionGetMediaType(formatDescriptor)
|
||||||
|
if type == kCMMediaType_Video {
|
||||||
|
self.impl.appendVideoSampleBuffer(sampleBuffer)
|
||||||
|
} else if type == kCMMediaType_Audio {
|
||||||
|
if self.configuration.hasAudio {
|
||||||
|
self.impl.appendAudioSampleBuffer(sampleBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
//! Project version number for Camera.
|
|
||||||
FOUNDATION_EXPORT double CameraVersionNumber;
|
|
||||||
|
|
||||||
//! Project version string for Camera.
|
|
||||||
FOUNDATION_EXPORT const unsigned char CameraVersionString[];
|
|
||||||
|
|
||||||
// In this header, you should import all the public headers of your framework using statements like #import <Camera/PublicHeader.h>
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
import SwiftSignalKit
|
|
||||||
|
|
||||||
final class CameraModeNode: ASDisplayNode {
|
|
||||||
enum Mode {
|
|
||||||
case photo
|
|
||||||
case video
|
|
||||||
case scan
|
|
||||||
}
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(mode: Mode, transition: ContainedViewLayoutTransition) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,227 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
|
|
||||||
private final class ZoomWheelNodeDrawingState: NSObject {
|
|
||||||
let transition: CGFloat
|
|
||||||
let reverse: Bool
|
|
||||||
|
|
||||||
init(transition: CGFloat, reverse: Bool) {
|
|
||||||
self.transition = transition
|
|
||||||
self.reverse = reverse
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ZoomWheelNode: ASDisplayNode {
|
|
||||||
class State: Equatable {
|
|
||||||
let active: Bool
|
|
||||||
|
|
||||||
init(active: Bool) {
|
|
||||||
self.active = active
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: State, rhs: State) -> Bool {
|
|
||||||
if lhs.active != rhs.active {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TransitionContext {
|
|
||||||
let startTime: Double
|
|
||||||
let duration: Double
|
|
||||||
let previousState: State
|
|
||||||
|
|
||||||
init(startTime: Double, duration: Double, previousState: State) {
|
|
||||||
self.startTime = startTime
|
|
||||||
self.duration = duration
|
|
||||||
self.previousState = previousState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var animator: ConstantDisplayLinkAnimator?
|
|
||||||
|
|
||||||
private var hasState = false
|
|
||||||
private var state: State = State(active: false)
|
|
||||||
private var transitionContext: TransitionContext?
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.isOpaque = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(state: State, animated: Bool) {
|
|
||||||
var animated = animated
|
|
||||||
if !self.hasState {
|
|
||||||
self.hasState = true
|
|
||||||
animated = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.state != state {
|
|
||||||
let previousState = self.state
|
|
||||||
self.state = state
|
|
||||||
|
|
||||||
if animated {
|
|
||||||
self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateAnimations()
|
|
||||||
self.setNeedsDisplay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAnimations() {
|
|
||||||
var animate = false
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
if let transitionContext = self.transitionContext {
|
|
||||||
if transitionContext.startTime + transitionContext.duration < timestamp {
|
|
||||||
self.transitionContext = nil
|
|
||||||
} else {
|
|
||||||
animate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if animate {
|
|
||||||
let animator: ConstantDisplayLinkAnimator
|
|
||||||
if let current = self.animator {
|
|
||||||
animator = current
|
|
||||||
} else {
|
|
||||||
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
||||||
self?.updateAnimations()
|
|
||||||
})
|
|
||||||
self.animator = animator
|
|
||||||
}
|
|
||||||
animator.isPaused = false
|
|
||||||
} else {
|
|
||||||
self.animator?.isPaused = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setNeedsDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
||||||
var transitionFraction: CGFloat = self.state.active ? 1.0 : 0.0
|
|
||||||
|
|
||||||
var reverse = false
|
|
||||||
if let transitionContext = self.transitionContext {
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
|
|
||||||
t = min(1.0, max(0.0, t))
|
|
||||||
|
|
||||||
if transitionContext.previousState.active != self.state.active {
|
|
||||||
transitionFraction = self.state.active ? t : 1.0 - t
|
|
||||||
|
|
||||||
reverse = transitionContext.previousState.active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ZoomWheelNodeDrawingState(transition: transitionFraction, reverse: reverse)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
|
||||||
let context = UIGraphicsGetCurrentContext()!
|
|
||||||
|
|
||||||
if !isRasterizing {
|
|
||||||
context.setBlendMode(.copy)
|
|
||||||
context.setFillColor(UIColor.clear.cgColor)
|
|
||||||
context.fill(bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let parameters = parameters as? ZoomWheelNodeDrawingState else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = UIColor(rgb: 0xffffff)
|
|
||||||
context.setFillColor(color.cgColor)
|
|
||||||
|
|
||||||
let clearLineWidth: CGFloat = 4.0
|
|
||||||
let lineWidth: CGFloat = 1.0 + UIScreenPixel
|
|
||||||
|
|
||||||
context.scaleBy(x: 2.5, y: 2.5)
|
|
||||||
|
|
||||||
context.translateBy(x: 4.0, y: 3.0)
|
|
||||||
let _ = try? drawSvgPath(context, path: "M14,8.335 C14.36727,8.335 14.665,8.632731 14.665,9 C14.665,11.903515 12.48064,14.296846 9.665603,14.626311 L9.665,16 C9.665,16.367269 9.367269,16.665 9,16.665 C8.666119,16.665 8.389708,16.418942 8.34221,16.098269 L8.335,16 L8.3354,14.626428 C5.519879,14.297415 3.335,11.90386 3.335,9 C3.335,8.632731 3.632731,8.335 4,8.335 C4.367269,8.335 4.665,8.632731 4.665,9 C4.665,11.394154 6.605846,13.335 9,13.335 C11.39415,13.335 13.335,11.394154 13.335,9 C13.335,8.632731 13.63273,8.335 14,8.335 Z ")
|
|
||||||
|
|
||||||
let _ = try? drawSvgPath(context, path: "M9,2.5 C10.38071,2.5 11.5,3.61929 11.5,5 L11.5,9 C11.5,10.380712 10.38071,11.5 9,11.5 C7.619288,11.5 6.5,10.380712 6.5,9 L6.5,5 C6.5,3.61929 7.619288,2.5 9,2.5 Z ")
|
|
||||||
|
|
||||||
context.translateBy(x: -4.0, y: -3.0)
|
|
||||||
|
|
||||||
if parameters.transition > 0.0 {
|
|
||||||
let startPoint: CGPoint
|
|
||||||
let endPoint: CGPoint
|
|
||||||
|
|
||||||
let origin = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel)
|
|
||||||
let length: CGFloat = 17.0
|
|
||||||
|
|
||||||
if parameters.reverse {
|
|
||||||
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
|
|
||||||
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
|
|
||||||
} else {
|
|
||||||
startPoint = origin
|
|
||||||
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setBlendMode(.clear)
|
|
||||||
context.setLineWidth(clearLineWidth)
|
|
||||||
|
|
||||||
context.move(to: startPoint)
|
|
||||||
context.addLine(to: endPoint)
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
context.setBlendMode(.normal)
|
|
||||||
context.setStrokeColor(color.cgColor)
|
|
||||||
context.setLineWidth(lineWidth)
|
|
||||||
context.setLineCap(.round)
|
|
||||||
context.setLineJoin(.round)
|
|
||||||
|
|
||||||
context.move(to: startPoint)
|
|
||||||
context.addLine(to: endPoint)
|
|
||||||
context.strokePath()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ButtonNode: HighlightTrackingButtonNode {
|
|
||||||
private let backgroundNode: ASDisplayNode
|
|
||||||
private let textNode: ImmediateTextNode
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
|
||||||
self.textNode = ImmediateTextNode()
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.backgroundNode)
|
|
||||||
self.addSubnode(self.textNode)
|
|
||||||
|
|
||||||
self.highligthedChanged = { [weak self] highlight in
|
|
||||||
if let strongSelf = self {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func update() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class CameraZoomNode: ASDisplayNode {
|
|
||||||
private let wheelNode: ZoomWheelNode
|
|
||||||
|
|
||||||
private let backgroundNode: ASDisplayNode
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
self.wheelNode = ZoomWheelNode()
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.wheelNode)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
swift_library(
|
swift_library(
|
||||||
name = "MeshAnimationCache",
|
name = "ChatContextQuery",
|
||||||
module_name = "MeshAnimationCache",
|
module_name = "ChatContextQuery",
|
||||||
srcs = glob([
|
srcs = glob([
|
||||||
"Sources/**/*.swift",
|
"Sources/**/*.swift",
|
||||||
]),
|
]),
|
||||||
@ -10,11 +10,10 @@ swift_library(
|
|||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"//submodules/LottieMeshSwift:LottieMeshSwift",
|
|
||||||
"//submodules/Postbox:Postbox",
|
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
"//submodules/GZip:GZip",
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
"//submodules/AppBundle:AppBundle",
|
"//submodules/TextFormat:TextFormat",
|
||||||
|
"//submodules/AccountContext:AccountContext",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
241
submodules/ChatContextQuery/Sources/ChatContextQuery.swift
Normal file
241
submodules/ChatContextQuery/Sources/ChatContextQuery.swift
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TextFormat
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
public struct PossibleContextQueryTypes: OptionSet {
|
||||||
|
public var rawValue: Int32
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
self.rawValue = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(rawValue: Int32) {
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let emoji = PossibleContextQueryTypes(rawValue: (1 << 0))
|
||||||
|
public static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 1))
|
||||||
|
public static let mention = PossibleContextQueryTypes(rawValue: (1 << 2))
|
||||||
|
public static let command = PossibleContextQueryTypes(rawValue: (1 << 3))
|
||||||
|
public static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4))
|
||||||
|
public static let emojiSearch = PossibleContextQueryTypes(rawValue: (1 << 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scalarCanPrependQueryControl(_ c: UnicodeScalar?) -> Bool {
|
||||||
|
if let c = c {
|
||||||
|
if c == " " || c == "\n" || c == "." || c == "," {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeScalar(_ c: Character) -> Character {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
private let spaceScalar = " " as UnicodeScalar
|
||||||
|
private let newlineScalar = "\n" as UnicodeScalar
|
||||||
|
private let hashScalar = "#" as UnicodeScalar
|
||||||
|
private let atScalar = "@" as UnicodeScalar
|
||||||
|
private let slashScalar = "/" as UnicodeScalar
|
||||||
|
private let colonScalar = ":" as UnicodeScalar
|
||||||
|
private let alphanumerics = CharacterSet.alphanumerics
|
||||||
|
|
||||||
|
public func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||||
|
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textInputStateContextQueryRangeAndType(inputText: NSAttributedString, selectionRange: Range<Int>) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||||
|
if selectionRange.count != 0 {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputString: NSString = inputText.string as NSString
|
||||||
|
var results: [(NSRange, PossibleContextQueryTypes, NSRange?)] = []
|
||||||
|
let inputLength = inputString.length
|
||||||
|
|
||||||
|
if inputLength != 0 {
|
||||||
|
if inputString.hasPrefix("@") && inputLength != 1 {
|
||||||
|
let startIndex = 1
|
||||||
|
var index = startIndex
|
||||||
|
var contextAddressRange: NSRange?
|
||||||
|
|
||||||
|
while true {
|
||||||
|
if index == inputLength {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||||
|
if c == " " {
|
||||||
|
if index != startIndex {
|
||||||
|
contextAddressRange = NSRange(location: startIndex, length: index - startIndex)
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
if !((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == inputLength {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let contextAddressRange = contextAddressRange {
|
||||||
|
results.append((contextAddressRange, [.contextRequest], NSRange(location: index, length: inputLength - index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxIndex = min(selectionRange.lowerBound, inputLength)
|
||||||
|
if maxIndex == 0 {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
var index = maxIndex - 1
|
||||||
|
|
||||||
|
var possibleQueryRange: NSRange?
|
||||||
|
|
||||||
|
let string = (inputString as String)
|
||||||
|
let trimmedString = string.trimmingTrailingSpaces()
|
||||||
|
if string.count < 3, trimmedString.isSingleEmoji {
|
||||||
|
if inputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil {
|
||||||
|
return [(NSRange(location: 0, length: inputString.length - (string.count - trimmedString.count)), [.emoji], nil)]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*let activeString = inputText.attributedSubstring(from: NSRange(location: 0, length: inputState.selectionRange.upperBound))
|
||||||
|
if let lastCharacter = activeString.string.last, String(lastCharacter).isSingleEmoji {
|
||||||
|
let matchLength = (String(lastCharacter) as NSString).length
|
||||||
|
|
||||||
|
if activeString.attribute(ChatTextInputAttributes.customEmoji, at: activeString.length - matchLength, effectiveRange: nil) == nil {
|
||||||
|
return [(NSRange(location: inputState.selectionRange.upperBound - matchLength, length: matchLength), [.emojiSearch], nil)]
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .emojiSearch])
|
||||||
|
var definedType = false
|
||||||
|
|
||||||
|
while true {
|
||||||
|
var previousC: UnicodeScalar?
|
||||||
|
if index != 0 {
|
||||||
|
previousC = UnicodeScalar(inputString.character(at: index - 1))
|
||||||
|
}
|
||||||
|
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||||
|
if c == spaceScalar || c == newlineScalar {
|
||||||
|
possibleTypes = []
|
||||||
|
} else if c == hashScalar {
|
||||||
|
if scalarCanPrependQueryControl(previousC) {
|
||||||
|
possibleTypes = possibleTypes.intersection([.hashtag])
|
||||||
|
definedType = true
|
||||||
|
index += 1
|
||||||
|
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if c == atScalar {
|
||||||
|
if scalarCanPrependQueryControl(previousC) {
|
||||||
|
possibleTypes = possibleTypes.intersection([.mention])
|
||||||
|
definedType = true
|
||||||
|
index += 1
|
||||||
|
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if c == slashScalar {
|
||||||
|
if scalarCanPrependQueryControl(previousC) {
|
||||||
|
possibleTypes = possibleTypes.intersection([.command])
|
||||||
|
definedType = true
|
||||||
|
index += 1
|
||||||
|
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if c == colonScalar {
|
||||||
|
if scalarCanPrependQueryControl(previousC) {
|
||||||
|
possibleTypes = possibleTypes.intersection([.emojiSearch])
|
||||||
|
definedType = true
|
||||||
|
index += 1
|
||||||
|
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
index -= 1
|
||||||
|
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let possibleQueryRange = possibleQueryRange, definedType && !possibleTypes.isEmpty {
|
||||||
|
results.append((possibleQueryRange, possibleTypes, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ChatPresentationInputQueryKind: Int32 {
|
||||||
|
case emoji
|
||||||
|
case hashtag
|
||||||
|
case mention
|
||||||
|
case command
|
||||||
|
case contextRequest
|
||||||
|
case emojiSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ChatInputQueryMentionTypes: OptionSet, Hashable {
|
||||||
|
public var rawValue: Int32
|
||||||
|
|
||||||
|
public init(rawValue: Int32) {
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let contextBots = ChatInputQueryMentionTypes(rawValue: 1 << 0)
|
||||||
|
public static let members = ChatInputQueryMentionTypes(rawValue: 1 << 1)
|
||||||
|
public static let accountPeer = ChatInputQueryMentionTypes(rawValue: 1 << 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||||
|
case emoji(String)
|
||||||
|
case hashtag(String)
|
||||||
|
case mention(query: String, types: ChatInputQueryMentionTypes)
|
||||||
|
case command(String)
|
||||||
|
case emojiSearch(query: String, languageCode: String, range: NSRange)
|
||||||
|
case contextRequest(addressName: String, query: String)
|
||||||
|
|
||||||
|
public var kind: ChatPresentationInputQueryKind {
|
||||||
|
switch self {
|
||||||
|
case .emoji:
|
||||||
|
return .emoji
|
||||||
|
case .hashtag:
|
||||||
|
return .hashtag
|
||||||
|
case .mention:
|
||||||
|
return .mention
|
||||||
|
case .command:
|
||||||
|
return .command
|
||||||
|
case .contextRequest:
|
||||||
|
return .contextRequest
|
||||||
|
case .emojiSearch:
|
||||||
|
return .emojiSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ChatContextQueryError {
|
||||||
|
case generic
|
||||||
|
case inlineBotLocationRequest(EnginePeer.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ChatContextQueryUpdate {
|
||||||
|
case remove
|
||||||
|
case update(ChatPresentationInputQuery, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError>)
|
||||||
|
}
|
@ -3,7 +3,6 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
@ -17,6 +16,26 @@ import ConfettiEffect
|
|||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import SolidRoundedButtonNode
|
import SolidRoundedButtonNode
|
||||||
|
|
||||||
|
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
|
||||||
|
if useTotalFileAllocatedSize {
|
||||||
|
let url = URL(fileURLWithPath: path)
|
||||||
|
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
|
||||||
|
if values.isRegularFile ?? false {
|
||||||
|
if let fileSize = values.totalFileAllocatedSize {
|
||||||
|
return Int64(fileSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = stat()
|
||||||
|
if stat(path, &value) == 0 {
|
||||||
|
return value.st_size
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ProgressEstimator {
|
private final class ProgressEstimator {
|
||||||
private var averageProgressPerSecond: Double = 0.0
|
private var averageProgressPerSecond: Double = 0.0
|
||||||
private var lastMeasurement: (Double, Float)?
|
private var lastMeasurement: (Double, Float)?
|
||||||
@ -91,7 +110,7 @@ private final class ImportManager {
|
|||||||
return self.statePromise.get()
|
return self.statePromise.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
init(account: Account, peerId: EnginePeer.Id, mainFile: EngineTempBox.File, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.archivePath = archivePath
|
self.archivePath = archivePath
|
||||||
self.entries = entries
|
self.entries = entries
|
||||||
@ -234,8 +253,8 @@ private final class ImportManager {
|
|||||||
|
|
||||||
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
|
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
|
||||||
|
|
||||||
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
|
let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
|
||||||
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
||||||
let startTime = CACurrentMediaTime()
|
let startTime = CACurrentMediaTime()
|
||||||
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
||||||
@ -440,9 +459,9 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
|
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
|
||||||
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
|
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
|
||||||
|
|
||||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)])
|
||||||
|
|
||||||
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
|
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
|
||||||
|
|
||||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||||
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
||||||
@ -724,9 +743,9 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
fileprivate let cancel: () -> Void
|
fileprivate let cancel: () -> Void
|
||||||
fileprivate var peerId: PeerId
|
fileprivate var peerId: EnginePeer.Id
|
||||||
private let archivePath: String?
|
private let archivePath: String?
|
||||||
private let mainEntry: TempBoxFile
|
private let mainEntry: EngineTempBox.File
|
||||||
private let totalBytes: Int64
|
private let totalBytes: Int64
|
||||||
private let totalMediaBytes: Int64
|
private let totalMediaBytes: Int64
|
||||||
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
|
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
|
||||||
@ -746,7 +765,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: EnginePeer.Id, archivePath: String?, mainEntry: EngineTempBox.File, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.cancel = cancel
|
self.cancel = cancel
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
self.progressEstimator = ProgressEstimator()
|
self.progressEstimator = ProgressEstimator()
|
||||||
self.beganCompletion = false
|
self.beganCompletion = false
|
||||||
|
|
||||||
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
|
let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
|
||||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||||
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|
||||||
|> mapError { _ -> ImportManager.ImportError in
|
|> mapError { _ -> ImportManager.ImportError in
|
||||||
|
@ -30,6 +30,7 @@ public enum ChatListSearchItemHeaderType {
|
|||||||
case downloading
|
case downloading
|
||||||
case recentDownloads
|
case recentDownloads
|
||||||
case topics
|
case topics
|
||||||
|
case text(String, AnyHashable)
|
||||||
|
|
||||||
fileprivate func title(strings: PresentationStrings) -> String {
|
fileprivate func title(strings: PresentationStrings) -> String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -87,6 +88,8 @@ public enum ChatListSearchItemHeaderType {
|
|||||||
return strings.DownloadList_DownloadedHeader
|
return strings.DownloadList_DownloadedHeader
|
||||||
case .topics:
|
case .topics:
|
||||||
return strings.DialogList_SearchSectionTopics
|
return strings.DialogList_SearchSectionTopics
|
||||||
|
case let .text(text, _):
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,11 +149,13 @@ public enum ChatListSearchItemHeaderType {
|
|||||||
return .recentDownloads
|
return .recentDownloads
|
||||||
case .topics:
|
case .topics:
|
||||||
return .topics
|
return .topics
|
||||||
|
case let .text(_, id):
|
||||||
|
return .text(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ChatListSearchItemHeaderId: Int32 {
|
private enum ChatListSearchItemHeaderId: Hashable {
|
||||||
case localPeers
|
case localPeers
|
||||||
case members
|
case members
|
||||||
case contacts
|
case contacts
|
||||||
@ -181,6 +186,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
|
|||||||
case downloading
|
case downloading
|
||||||
case recentDownloads
|
case recentDownloads
|
||||||
case topics
|
case topics
|
||||||
|
case text(AnyHashable)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||||
@ -197,7 +203,7 @@ public final class ChatListSearchItemHeader: ListViewItemHeader {
|
|||||||
|
|
||||||
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) {
|
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.type.id.rawValue))
|
self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.type.id.hashValue))
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.actionTitle = actionTitle
|
self.actionTitle = actionTitle
|
||||||
|
@ -10,7 +10,6 @@ import HorizontalPeerItem
|
|||||||
import ListSectionHeaderNode
|
import ListSectionHeaderNode
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import Postbox
|
|
||||||
|
|
||||||
private func calculateItemCustomWidth(width: CGFloat) -> CGFloat {
|
private func calculateItemCustomWidth(width: CGFloat) -> CGFloat {
|
||||||
let itemInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 6.0)
|
let itemInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 6.0)
|
||||||
@ -160,17 +159,19 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
|
|||||||
return .single(([], [:], [:]))
|
return .single(([], [:], [:]))
|
||||||
case let .peers(peers):
|
case let .peers(peers):
|
||||||
return combineLatest(queue: .mainQueue(),
|
return combineLatest(queue: .mainQueue(),
|
||||||
peers.filter {
|
peers.filter {
|
||||||
!$0.isDeleted
|
!$0.isDeleted
|
||||||
}.map {
|
}.map {
|
||||||
context.account.postbox.peerView(id: $0.id)
|
context.account.postbox.peerView(id: $0.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> mapToSignal { peerViews -> Signal<([EnginePeer], [EnginePeer.Id: (Int32, Bool)], [EnginePeer.Id: EnginePeer.Presence]), NoError> in
|
|> mapToSignal { peerViews -> Signal<([EnginePeer], [EnginePeer.Id: (Int32, Bool)], [EnginePeer.Id: EnginePeer.Presence]), NoError> in
|
||||||
return context.account.postbox.unreadMessageCountsView(items: peerViews.map { item -> UnreadMessageCountsItem in
|
return context.engine.data.subscribe(
|
||||||
return UnreadMessageCountsItem.peer(id: item.peerId, handleThreads: true)
|
EngineDataMap(peerViews.map { item in
|
||||||
})
|
return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: item.peerId)
|
||||||
|> map { values in
|
})
|
||||||
|
)
|
||||||
|
|> map { unreadCounts in
|
||||||
var peers: [EnginePeer] = []
|
var peers: [EnginePeer] = []
|
||||||
var unread: [EnginePeer.Id: (Int32, Bool)] = [:]
|
var unread: [EnginePeer.Id: (Int32, Bool)] = [:]
|
||||||
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||||
@ -186,9 +187,9 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let unreadCount = values.count(for: .peer(id: peerView.peerId, handleThreads: true))
|
let unreadCount = unreadCounts[peerView.peerId]
|
||||||
if let unreadCount = unreadCount, unreadCount > 0 {
|
if let unreadCount, unreadCount > 0 {
|
||||||
unread[peerView.peerId] = (unreadCount, isMuted)
|
unread[peerView.peerId] = (Int32(unreadCount), isMuted)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let presence = peerView.peerPresences[peer.id] {
|
if let presence = peerView.peerPresences[peer.id] {
|
||||||
|
@ -93,6 +93,12 @@ swift_library(
|
|||||||
"//submodules/ItemListUI",
|
"//submodules/ItemListUI",
|
||||||
"//submodules/QrCodeUI",
|
"//submodules/QrCodeUI",
|
||||||
"//submodules/TelegramUI/Components/ActionPanelComponent",
|
"//submodules/TelegramUI/Components/ActionPanelComponent",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||||
|
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||||
|
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
|
||||||
|
"//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -723,6 +723,20 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) |> deliverOnMainQueue
|
return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) |> deliverOnMainQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updatePeerStoriesMuted: (PeerId, PeerStoryNotificationSettings.Mute) -> Signal<Void, NoError> = {
|
||||||
|
peerId, mute in
|
||||||
|
return context.engine.peers.updatePeerStoriesMutedSetting(peerId: peerId, mute: mute) |> deliverOnMainQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatePeerStoriesHideSender: (PeerId, PeerStoryNotificationSettings.HideSender) -> Signal<Void, NoError> = {
|
||||||
|
peerId, hideSender in
|
||||||
|
return context.engine.peers.updatePeerStoriesHideSenderSetting(peerId: peerId, hideSender: hideSender) |> deliverOnMainQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatePeerStorySound: (PeerId, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
|
||||||
|
return context.engine.peers.updatePeerStorySoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue
|
||||||
|
}
|
||||||
|
|
||||||
let defaultSound: PeerMessageSound
|
let defaultSound: PeerMessageSound
|
||||||
|
|
||||||
if case .broadcast = channel.info {
|
if case .broadcast = channel.info {
|
||||||
@ -733,7 +747,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
|
|
||||||
let canRemove = false
|
let canRemove = false
|
||||||
|
|
||||||
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: channel, threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
|
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
|
||||||
let _ = (updatePeerSound(peerId, sound)
|
let _ = (updatePeerSound(peerId, sound)
|
||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
})
|
})
|
||||||
@ -756,6 +770,15 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
|
|
||||||
})
|
})
|
||||||
|
}, updatePeerStoriesMuted: { peerId, mute in
|
||||||
|
let _ = (updatePeerStoriesMuted(peerId, mute)
|
||||||
|
|> deliverOnMainQueue).start()
|
||||||
|
}, updatePeerStoriesHideSender: { peerId, hideSender in
|
||||||
|
let _ = (updatePeerStoriesHideSender(peerId, hideSender)
|
||||||
|
|> deliverOnMainQueue).start()
|
||||||
|
}, updatePeerStorySound: { peerId, sound in
|
||||||
|
let _ = (updatePeerStorySound(peerId, sound)
|
||||||
|
|> deliverOnMainQueue).start()
|
||||||
}, removePeerFromExceptions: {
|
}, removePeerFromExceptions: {
|
||||||
}, modifiedPeer: {
|
}, modifiedPeer: {
|
||||||
})
|
})
|
||||||
|
@ -45,8 +45,12 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
|||||||
self.action = action
|
self.action = action
|
||||||
|
|
||||||
switch appearance {
|
switch appearance {
|
||||||
case .option:
|
case let .option(sectionTitle):
|
||||||
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
if let sectionTitle {
|
||||||
|
self.header = ChatListSearchItemHeader(type: .text(sectionTitle, AnyHashable(0)), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||||
|
} else {
|
||||||
|
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||||
|
}
|
||||||
case .action:
|
case .action:
|
||||||
self.header = header
|
self.header = header
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -9,15 +9,27 @@ import AppBundle
|
|||||||
import SolidRoundedButtonNode
|
import SolidRoundedButtonNode
|
||||||
import ActivityIndicator
|
import ActivityIndicator
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import ComponentFlow
|
||||||
|
import ArchiveInfoScreen
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ChatListHeaderComponent
|
||||||
|
|
||||||
final class ChatListEmptyNode: ASDisplayNode {
|
final class ChatListEmptyNode: ASDisplayNode {
|
||||||
enum Subject {
|
enum Subject {
|
||||||
case chats(hasArchive: Bool)
|
case chats(hasArchive: Bool)
|
||||||
|
case archive
|
||||||
case filter(showEdit: Bool)
|
case filter(showEdit: Bool)
|
||||||
case forum(hasGeneral: Bool)
|
case forum(hasGeneral: Bool)
|
||||||
}
|
}
|
||||||
private let action: () -> Void
|
private let action: () -> Void
|
||||||
private let secondaryAction: () -> Void
|
private let secondaryAction: () -> Void
|
||||||
|
private let openArchiveSettings: () -> Void
|
||||||
|
|
||||||
|
private let context: AccountContext
|
||||||
|
private var theme: PresentationTheme
|
||||||
|
private var strings: PresentationStrings
|
||||||
|
|
||||||
let subject: Subject
|
let subject: Subject
|
||||||
private(set) var isLoading: Bool
|
private(set) var isLoading: Bool
|
||||||
@ -28,14 +40,25 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
private let secondaryButtonNode: HighlightableButtonNode
|
private let secondaryButtonNode: HighlightableButtonNode
|
||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
|
|
||||||
|
private var emptyArchive: ComponentView<Empty>?
|
||||||
|
|
||||||
private var animationSize: CGSize = CGSize()
|
private var animationSize: CGSize = CGSize()
|
||||||
private var buttonIsHidden: Bool
|
private var buttonIsHidden: Bool
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||||
|
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
|
||||||
|
|
||||||
init(context: AccountContext, subject: Subject, isLoading: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) {
|
private var globalPrivacySettings: GlobalPrivacySettings = .default
|
||||||
|
private var archiveSettingsDisposable: Disposable?
|
||||||
|
|
||||||
|
init(context: AccountContext, subject: Subject, isLoading: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, secondaryAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
|
||||||
self.action = action
|
self.action = action
|
||||||
self.secondaryAction = secondaryAction
|
self.secondaryAction = secondaryAction
|
||||||
|
self.openArchiveSettings = openArchiveSettings
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.isLoading = isLoading
|
self.isLoading = isLoading
|
||||||
|
|
||||||
@ -80,16 +103,20 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.animationNode)
|
|
||||||
self.addSubnode(self.textNode)
|
|
||||||
self.addSubnode(self.descriptionNode)
|
|
||||||
self.addSubnode(self.buttonNode)
|
|
||||||
self.addSubnode(self.secondaryButtonNode)
|
|
||||||
self.addSubnode(self.activityIndicator)
|
|
||||||
|
|
||||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
||||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||||
self.animationNode.visibility = true
|
|
||||||
|
if case .archive = subject {
|
||||||
|
} else {
|
||||||
|
self.addSubnode(self.animationNode)
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.descriptionNode)
|
||||||
|
self.addSubnode(self.buttonNode)
|
||||||
|
self.addSubnode(self.secondaryButtonNode)
|
||||||
|
self.addSubnode(self.activityIndicator)
|
||||||
|
|
||||||
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||||
|
self.animationNode.visibility = true
|
||||||
|
}
|
||||||
|
|
||||||
self.animationNode.isHidden = self.isLoading
|
self.animationNode.isHidden = self.isLoading
|
||||||
self.textNode.isHidden = self.isLoading
|
self.textNode.isHidden = self.isLoading
|
||||||
@ -107,6 +134,27 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||||
|
|
||||||
self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:))))
|
self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:))))
|
||||||
|
|
||||||
|
if case .archive = subject {
|
||||||
|
let _ = self.context.engine.privacy.updateGlobalPrivacySettings().start()
|
||||||
|
|
||||||
|
self.archiveSettingsDisposable = (context.engine.data.subscribe(
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] settings in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.globalPrivacySettings = settings
|
||||||
|
if let (size, insets) = self.validLayout {
|
||||||
|
self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.archiveSettingsDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func buttonPressed() {
|
@objc private func buttonPressed() {
|
||||||
@ -130,13 +178,19 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
var descriptionText = ""
|
var descriptionText = ""
|
||||||
let buttonText: String
|
let buttonText: String?
|
||||||
switch self.subject {
|
switch self.subject {
|
||||||
case let .chats(hasArchive):
|
case let .chats(hasArchive):
|
||||||
text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList
|
text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList
|
||||||
buttonText = strings.ChatList_EmptyChatListNewMessage
|
buttonText = strings.ChatList_EmptyChatListNewMessage
|
||||||
|
case .archive:
|
||||||
|
text = strings.ChatList_EmptyChatList
|
||||||
|
buttonText = nil
|
||||||
case .filter:
|
case .filter:
|
||||||
text = strings.ChatList_EmptyChatListFilterTitle
|
text = strings.ChatList_EmptyChatListFilterTitle
|
||||||
descriptionText = strings.ChatList_EmptyChatListFilterText
|
descriptionText = strings.ChatList_EmptyChatListFilterText
|
||||||
@ -152,12 +206,21 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.textNode.attributedText = string
|
self.textNode.attributedText = string
|
||||||
self.descriptionNode.attributedText = descriptionString
|
self.descriptionNode.attributedText = descriptionString
|
||||||
|
|
||||||
self.buttonNode.title = buttonText
|
if let buttonText {
|
||||||
|
self.buttonNode.title = buttonText
|
||||||
|
self.buttonNode.isHidden = false
|
||||||
|
} else {
|
||||||
|
self.buttonNode.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false)
|
self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false)
|
||||||
|
|
||||||
if let size = self.validLayout {
|
if let (size, insets) = self.validLayout {
|
||||||
self.updateLayout(size: size, transition: .immediate)
|
self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||||
|
|
||||||
|
if let scrollingOffset = self.scrollingOffset {
|
||||||
|
self.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,17 +236,17 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.activityIndicator.isHidden = !self.isLoading
|
self.activityIndicator.isHidden = !self.isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||||
self.validLayout = size
|
self.validLayout = (size, insets)
|
||||||
|
|
||||||
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
||||||
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
|
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: insets.top + floor((size.height - insets.top - insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
|
||||||
|
|
||||||
let animationSpacing: CGFloat = 24.0
|
let animationSpacing: CGFloat = 24.0
|
||||||
let descriptionSpacing: CGFloat = 8.0
|
let descriptionSpacing: CGFloat = 8.0
|
||||||
|
|
||||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height))
|
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height - insets.top - insets.bottom))
|
||||||
let descriptionSize = self.descriptionNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height))
|
let descriptionSize = self.descriptionNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height - insets.top - insets.bottom))
|
||||||
|
|
||||||
let buttonSideInset: CGFloat = 32.0
|
let buttonSideInset: CGFloat = 32.0
|
||||||
let buttonWidth = min(270.0, size.width - buttonSideInset * 2.0)
|
let buttonWidth = min(270.0, size.width - buttonSideInset * 2.0)
|
||||||
@ -199,7 +262,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
|
|
||||||
let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSize.height
|
let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSize.height
|
||||||
var contentOffset: CGFloat = 0.0
|
var contentOffset: CGFloat = 0.0
|
||||||
if size.height < contentHeight + threshold {
|
if size.height - insets.top - insets.bottom < contentHeight + threshold {
|
||||||
contentOffset = -self.animationSize.height - animationSpacing + 44.0
|
contentOffset = -self.animationSize.height - animationSpacing + 44.0
|
||||||
transition.updateAlpha(node: self.animationNode, alpha: 0.0)
|
transition.updateAlpha(node: self.animationNode, alpha: 0.0)
|
||||||
} else {
|
} else {
|
||||||
@ -207,7 +270,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
transition.updateAlpha(node: self.animationNode, alpha: 1.0)
|
transition.updateAlpha(node: self.animationNode, alpha: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - self.animationSize.width) / 2.0), y: floor((size.height - contentHeight) / 2.0) + contentOffset), size: self.animationSize)
|
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - self.animationSize.width) / 2.0), y: insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) + contentOffset), size: self.animationSize)
|
||||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: animationFrame.maxY + animationSpacing), size: textSize)
|
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: animationFrame.maxY + animationSpacing), size: textSize)
|
||||||
let descriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: textFrame.maxY + descriptionSpacing), size: descriptionSize)
|
let descriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: textFrame.maxY + descriptionSpacing), size: descriptionSize)
|
||||||
|
|
||||||
@ -221,7 +284,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
|
|
||||||
var bottomInset: CGFloat = 16.0
|
var bottomInset: CGFloat = 16.0
|
||||||
|
|
||||||
let secondaryButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - secondaryButtonSize.width) / 2.0), y: size.height - secondaryButtonSize.height - bottomInset), size: secondaryButtonSize)
|
let secondaryButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - secondaryButtonSize.width) / 2.0), y: size.height - insets.bottom - secondaryButtonSize.height - bottomInset), size: secondaryButtonSize)
|
||||||
transition.updateFrame(node: self.secondaryButtonNode, frame: secondaryButtonFrame)
|
transition.updateFrame(node: self.secondaryButtonNode, frame: secondaryButtonFrame)
|
||||||
|
|
||||||
if secondaryButtonSize.height > 0.0 {
|
if secondaryButtonSize.height > 0.0 {
|
||||||
@ -232,11 +295,68 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
if case .forum = self.subject {
|
if case .forum = self.subject {
|
||||||
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: descriptionFrame.maxY + 20.0), size: buttonSize)
|
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: descriptionFrame.maxY + 20.0), size: buttonSize)
|
||||||
} else {
|
} else {
|
||||||
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonHeight - bottomInset), size: buttonSize)
|
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - insets.bottom - buttonHeight - bottomInset), size: buttonSize)
|
||||||
}
|
}
|
||||||
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.scrollingOffset = (navigationHeight, offset)
|
||||||
|
|
||||||
|
guard let (size, _) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .archive = self.subject {
|
||||||
|
let emptyArchive: ComponentView<Empty>
|
||||||
|
if let current = self.emptyArchive {
|
||||||
|
emptyArchive = current
|
||||||
|
} else {
|
||||||
|
emptyArchive = ComponentView()
|
||||||
|
self.emptyArchive = emptyArchive
|
||||||
|
}
|
||||||
|
let emptyArchiveSize = emptyArchive.update(
|
||||||
|
transition: Transition(transition),
|
||||||
|
component: AnyComponent(ArchiveInfoContentComponent(
|
||||||
|
theme: self.theme,
|
||||||
|
strings: self.strings,
|
||||||
|
settings: self.globalPrivacySettings,
|
||||||
|
openSettings: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openArchiveSettings()
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {
|
||||||
|
},
|
||||||
|
containerSize: CGSize(width: size.width, height: 10000.0)
|
||||||
|
)
|
||||||
|
if let emptyArchiveView = emptyArchive.view {
|
||||||
|
if emptyArchiveView.superview == nil {
|
||||||
|
self.view.addSubview(emptyArchiveView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelledOutHeight: CGFloat = max(0.0, ChatListNavigationBar.searchScrollHeight - offset)
|
||||||
|
let visibleNavigationHeight: CGFloat = navigationHeight - ChatListNavigationBar.searchScrollHeight + cancelledOutHeight
|
||||||
|
|
||||||
|
let additionalOffset = min(0.0, -offset + ChatListNavigationBar.searchScrollHeight)
|
||||||
|
|
||||||
|
var archiveFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleNavigationHeight + floorToScreenPixels((size.height - visibleNavigationHeight - emptyArchiveSize.height - 50.0) * 0.5)), size: emptyArchiveSize)
|
||||||
|
archiveFrame.origin.y = max(archiveFrame.origin.y, visibleNavigationHeight + 20.0)
|
||||||
|
|
||||||
|
if size.height - visibleNavigationHeight - emptyArchiveSize.height - 20.0 < 0.0 {
|
||||||
|
archiveFrame.origin.y += additionalOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(view: emptyArchiveView, frame: archiveFrame)
|
||||||
|
}
|
||||||
|
} else if let emptyArchive = self.emptyArchive {
|
||||||
|
self.emptyArchive = nil
|
||||||
|
emptyArchive.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.buttonNode.frame.contains(point) {
|
if self.buttonNode.frame.contains(point) {
|
||||||
return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event)
|
return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event)
|
||||||
@ -244,6 +364,11 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
if self.secondaryButtonNode.frame.contains(point), !self.secondaryButtonNode.isHidden {
|
if self.secondaryButtonNode.frame.contains(point), !self.secondaryButtonNode.isHidden {
|
||||||
return self.secondaryButtonNode.view.hitTest(self.view.convert(point, to: self.secondaryButtonNode.view), with: event)
|
return self.secondaryButtonNode.view.hitTest(self.view.convert(point, to: self.secondaryButtonNode.view), with: event)
|
||||||
}
|
}
|
||||||
|
if let emptyArchiveView = self.emptyArchive?.view {
|
||||||
|
if let result = emptyArchiveView.hitTest(self.view.convert(point, to: emptyArchiveView), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
@ -30,8 +29,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
||||||
let openAddIncludePeer: () -> Void
|
let openAddIncludePeer: () -> Void
|
||||||
let openAddExcludePeer: () -> Void
|
let openAddExcludePeer: () -> Void
|
||||||
let deleteIncludePeer: (PeerId) -> Void
|
let deleteIncludePeer: (EnginePeer.Id) -> Void
|
||||||
let deleteExcludePeer: (PeerId) -> Void
|
let deleteExcludePeer: (EnginePeer.Id) -> Void
|
||||||
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
|
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
|
||||||
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
|
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
|
||||||
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
|
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
|
||||||
@ -49,8 +48,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
||||||
openAddIncludePeer: @escaping () -> Void,
|
openAddIncludePeer: @escaping () -> Void,
|
||||||
openAddExcludePeer: @escaping () -> Void,
|
openAddExcludePeer: @escaping () -> Void,
|
||||||
deleteIncludePeer: @escaping (PeerId) -> Void,
|
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||||
deleteExcludePeer: @escaping (PeerId) -> Void,
|
deleteExcludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||||
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
|
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
|
||||||
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
|
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
|
||||||
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
|
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
|
||||||
@ -93,7 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 {
|
|||||||
|
|
||||||
private enum ChatListFilterPresetEntryStableId: Hashable {
|
private enum ChatListFilterPresetEntryStableId: Hashable {
|
||||||
case index(Int)
|
case index(Int)
|
||||||
case peer(PeerId)
|
case peer(EnginePeer.Id)
|
||||||
case includePeerInfo
|
case includePeerInfo
|
||||||
case excludePeerInfo
|
case excludePeerInfo
|
||||||
case includeCategory(ChatListFilterIncludeCategory)
|
case includeCategory(ChatListFilterIncludeCategory)
|
||||||
@ -311,7 +310,7 @@ private extension ChatListFilterCategoryIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private enum ChatListFilterRevealedItemId: Equatable {
|
private enum ChatListFilterRevealedItemId: Equatable {
|
||||||
case peer(PeerId)
|
case peer(EnginePeer.Id)
|
||||||
case includeCategory(ChatListFilterIncludeCategory)
|
case includeCategory(ChatListFilterIncludeCategory)
|
||||||
case excludeCategory(ChatListFilterExcludeCategory)
|
case excludeCategory(ChatListFilterExcludeCategory)
|
||||||
}
|
}
|
||||||
@ -573,8 +572,8 @@ private struct ChatListFilterPresetControllerState: Equatable {
|
|||||||
var excludeMuted: Bool
|
var excludeMuted: Bool
|
||||||
var excludeRead: Bool
|
var excludeRead: Bool
|
||||||
var excludeArchived: Bool
|
var excludeArchived: Bool
|
||||||
var additionallyIncludePeers: [PeerId]
|
var additionallyIncludePeers: [EnginePeer.Id]
|
||||||
var additionallyExcludePeers: [PeerId]
|
var additionallyExcludePeers: [EnginePeer.Id]
|
||||||
|
|
||||||
var revealedItemId: ChatListFilterRevealedItemId?
|
var revealedItemId: ChatListFilterRevealedItemId?
|
||||||
var expandedSections: Set<FilterSection>
|
var expandedSections: Set<FilterSection>
|
||||||
@ -825,7 +824,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var includePeers: [PeerId] = []
|
var includePeers: [EnginePeer.Id] = []
|
||||||
for peerId in peerIds {
|
for peerId in peerIds {
|
||||||
switch peerId {
|
switch peerId {
|
||||||
case let .peer(id):
|
case let .peer(id):
|
||||||
@ -838,7 +837,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
|||||||
|
|
||||||
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
|
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
|
||||||
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
||||||
var removedPeers: [PeerId] = []
|
var removedPeers: [EnginePeer.Id] = []
|
||||||
if let data = filter.data {
|
if let data = filter.data {
|
||||||
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
||||||
}
|
}
|
||||||
@ -951,7 +950,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludePeers: [PeerId] = []
|
var excludePeers: [EnginePeer.Id] = []
|
||||||
for peerId in peerIds {
|
for peerId in peerIds {
|
||||||
switch peerId {
|
switch peerId {
|
||||||
case let .peer(id):
|
case let .peer(id):
|
||||||
@ -1144,7 +1143,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
|
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
let currentPeers = Atomic<[EnginePeer.Id: EngineRenderedPeer]>(value: [:])
|
||||||
let stateWithPeers = statePromise.get()
|
let stateWithPeers = statePromise.get()
|
||||||
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
|
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
|
||||||
let currentPeersValue = currentPeers.with { $0 }
|
let currentPeersValue = currentPeers.with { $0 }
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -39,7 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 {
|
|||||||
case list
|
case list
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
||||||
if peers.isEmpty {
|
if peers.isEmpty {
|
||||||
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
||||||
} else {
|
} else {
|
||||||
@ -197,7 +196,7 @@ private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [I
|
|||||||
return sortedFilters
|
return sortedFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] {
|
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] {
|
||||||
var entries: [ChatListFilterPresetListEntry] = []
|
var entries: [ChatListFilterPresetListEntry] = []
|
||||||
|
|
||||||
entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info))
|
entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info))
|
||||||
@ -522,7 +521,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
let limits = allLimits.0
|
let limits = allLimits.0
|
||||||
let premiumLimits = allLimits.1
|
let premiumLimits = allLimits.1
|
||||||
|
|
||||||
let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings]?.get(ChatListFilterSettings.self) ?? ChatListFilterSettings.default
|
|
||||||
let leftNavigationButton: ItemListNavigationButton?
|
let leftNavigationButton: ItemListNavigationButton?
|
||||||
switch mode {
|
switch mode {
|
||||||
case .default:
|
case .default:
|
||||||
@ -590,7 +588,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
|
||||||
|
|
||||||
return (controllerState, (listState, arguments))
|
return (controllerState, (listState, arguments))
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import ItemListUI
|
import ItemListUI
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
@ -85,6 +84,7 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
private var isDisabled: Bool = false
|
private var isDisabled: Bool = false
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
|
private var currentTitle: (String, String)?
|
||||||
|
|
||||||
private var pointerInteraction: PointerInteraction?
|
private var pointerInteraction: PointerInteraction?
|
||||||
|
|
||||||
@ -198,16 +198,34 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
self.isEditing = isEditing
|
self.isEditing = isEditing
|
||||||
self.isDisabled = isDisabled
|
self.isDisabled = isDisabled
|
||||||
|
|
||||||
|
var themeUpdated = false
|
||||||
if self.theme !== presentationData.theme {
|
if self.theme !== presentationData.theme {
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
|
|
||||||
self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor)
|
self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor)
|
||||||
self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor)
|
self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor)
|
||||||
|
|
||||||
|
themeUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleUpdated = false
|
||||||
|
if self.currentTitle?.0 != title || self.currentTitle?.1 != shortTitle {
|
||||||
|
self.currentTitle = (title, shortTitle)
|
||||||
|
|
||||||
|
titleUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var unreadCountUpdated = false
|
||||||
|
if self.unreadCount != unreadCount {
|
||||||
|
unreadCountUpdated = true
|
||||||
|
self.unreadCount = unreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
self.buttonNode.accessibilityLabel = title
|
self.buttonNode.accessibilityLabel = title
|
||||||
if unreadCount > 0 {
|
if unreadCount > 0 {
|
||||||
self.buttonNode.accessibilityValue = strings.VoiceOver_Chat_UnreadMessages(Int32(unreadCount))
|
if self.buttonNode.accessibilityValue == nil || unreadCountUpdated {
|
||||||
|
self.buttonNode.accessibilityValue = strings.VoiceOver_Chat_UnreadMessages(Int32(unreadCount))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.buttonNode.accessibilityValue = ""
|
self.buttonNode.accessibilityValue = ""
|
||||||
}
|
}
|
||||||
@ -253,14 +271,19 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
transition.updateAlpha(node: self.shortTitleNode, alpha: deselectionAlpha)
|
transition.updateAlpha(node: self.shortTitleNode, alpha: deselectionAlpha)
|
||||||
transition.updateAlpha(node: self.shortTitleActiveNode, alpha: selectionAlpha)
|
transition.updateAlpha(node: self.shortTitleActiveNode, alpha: selectionAlpha)
|
||||||
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
if themeUpdated || titleUpdated {
|
||||||
self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
|
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||||
self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||||
self.shortTitleActiveNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
|
self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||||
|
self.shortTitleActiveNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||||
|
}
|
||||||
|
|
||||||
if unreadCount != 0 {
|
if unreadCount != 0 {
|
||||||
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
if themeUpdated || unreadCountUpdated || self.badgeTextNode.attributedText == nil {
|
||||||
let badgeSelectionFraction: CGFloat = unreadHasUnmuted ? 1.0 : selectionFraction
|
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
let badgeSelectionFraction: CGFloat = unreadHasUnmuted ? 1.0 : selectionFraction
|
||||||
let badgeSelectionAlpha: CGFloat = badgeSelectionFraction
|
let badgeSelectionAlpha: CGFloat = badgeSelectionFraction
|
||||||
//let badgeDeselectionAlpha: CGFloat = 1.0 - badgeSelectionFraction
|
//let badgeDeselectionAlpha: CGFloat = 1.0 - badgeSelectionFraction
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1326,7 +1326,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
if !entities.isEmpty {
|
if !entities.isEmpty {
|
||||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||||
}
|
}
|
||||||
result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1364,12 +1364,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
|> deliverOnMainQueue).start())
|
|> deliverOnMainQueue).start())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if let secretPeer = peer as? TelegramSecretChat {
|
if case let .secretChat(secretPeer) = peer {
|
||||||
if let peer = peerMap[secretPeer.regularPeerId] {
|
if let peer = peerMap[secretPeer.regularPeerId] {
|
||||||
displayPeers.append(EnginePeer(peer))
|
displayPeers.append(peer)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
displayPeers.append(EnginePeer(peer))
|
displayPeers.append(peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -797,7 +797,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
|||||||
hasFailedMessages: false,
|
hasFailedMessages: false,
|
||||||
forumTopicData: nil,
|
forumTopicData: nil,
|
||||||
topForumTopicItems: [],
|
topForumTopicItems: [],
|
||||||
autoremoveTimeout: nil
|
autoremoveTimeout: nil,
|
||||||
|
storyState: nil
|
||||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
)), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||||
}
|
}
|
||||||
case let .addContact(phoneNumber, theme, strings):
|
case let .addContact(phoneNumber, theme, strings):
|
||||||
@ -2068,7 +2069,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
interaction.dismissInput()
|
interaction.dismissInput()
|
||||||
}, present: { c, a in
|
}, present: { c, a in
|
||||||
interaction.present(c, a)
|
interaction.present(c, a)
|
||||||
}, transitionNode: { messageId, media in
|
}, transitionNode: { messageId, media, _ in
|
||||||
return transitionNodeImpl?(messageId, EngineMedia(media))
|
return transitionNodeImpl?(messageId, EngineMedia(media))
|
||||||
}, addToTransitionSurface: { view in
|
}, addToTransitionSurface: { view in
|
||||||
addToTransitionSurfaceImpl?(view)
|
addToTransitionSurfaceImpl?(view)
|
||||||
@ -2166,6 +2167,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
}, openPremiumIntro: {
|
}, openPremiumIntro: {
|
||||||
}, openChatFolderUpdates: {
|
}, openChatFolderUpdates: {
|
||||||
}, hideChatFolderUpdates: {
|
}, hideChatFolderUpdates: {
|
||||||
|
}, openStories: { _, _ in
|
||||||
})
|
})
|
||||||
chatListInteraction.isSearchMode = true
|
chatListInteraction.isSearchMode = true
|
||||||
|
|
||||||
@ -2202,12 +2204,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
interaction.dismissInput()
|
interaction.dismissInput()
|
||||||
}, present: { c, a in
|
}, present: { c, a in
|
||||||
interaction.present(c, a)
|
interaction.present(c, a)
|
||||||
}, transitionNode: { messageId, media in
|
}, transitionNode: { messageId, media, _ in
|
||||||
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.listNode.forEachItemNode { itemNode in
|
strongSelf.listNode.forEachItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? ListMessageNode {
|
if let itemNode = itemNode as? ListMessageNode {
|
||||||
if let result = itemNode.transitionNode(id: messageId, media: media) {
|
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
|
||||||
transitionNode = result
|
transitionNode = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3029,7 +3031,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||||
self.listNode.forEachItemNode { itemNode in
|
self.listNode.forEachItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? ListMessageNode {
|
if let itemNode = itemNode as? ListMessageNode {
|
||||||
if let result = itemNode.transitionNode(id: messageId, media: media._asMedia()) {
|
if let result = itemNode.transitionNode(id: messageId, media: media._asMedia(), adjustRect: false) {
|
||||||
transitionNode = result
|
transitionNode = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3242,8 +3244,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
switch item.content {
|
switch item.content {
|
||||||
case let .peer(peerData):
|
case let .peer(peerData):
|
||||||
return (selectedItemNode.view, bounds, peerData.messages.last?.id ?? peerData.peer.peerId)
|
return (selectedItemNode.view, bounds, peerData.messages.last?.id ?? peerData.peer.peerId)
|
||||||
case let .groupReference(groupId, _, _, _, _):
|
case let .groupReference(groupReference):
|
||||||
return (selectedItemNode.view, bounds, groupId)
|
return (selectedItemNode.view, bounds, groupReference.groupId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -3366,13 +3368,13 @@ private final class ShimmerEffectNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ChatListSearchShimmerNode: ASDisplayNode {
|
public final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||||
private let backgroundColorNode: ASDisplayNode
|
private let backgroundColorNode: ASDisplayNode
|
||||||
private let effectNode: ShimmerEffectNode
|
private let effectNode: ShimmerEffectNode
|
||||||
private let maskNode: ASImageNode
|
private let maskNode: ASImageNode
|
||||||
private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)?
|
private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)?
|
||||||
|
|
||||||
init(key: ChatListSearchPaneKey) {
|
public init(key: ChatListSearchPaneKey) {
|
||||||
self.backgroundColorNode = ASDisplayNode()
|
self.backgroundColorNode = ASDisplayNode()
|
||||||
self.effectNode = ShimmerEffectNode()
|
self.effectNode = ShimmerEffectNode()
|
||||||
self.maskNode = ASImageNode()
|
self.maskNode = ASImageNode()
|
||||||
@ -3386,13 +3388,13 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.maskNode)
|
self.addSubnode(self.maskNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(context: AccountContext, size: CGSize, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) {
|
public func update(context: AccountContext, size: CGSize, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData || self.currentParams?.key != key {
|
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData || self.currentParams?.key != key {
|
||||||
self.currentParams = (size, presentationData, key)
|
self.currentParams = (size, presentationData, key)
|
||||||
|
|
||||||
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||||
|
|
||||||
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: []))
|
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil))
|
||||||
let timestamp1: Int32 = 100000
|
let timestamp1: Int32 = 100000
|
||||||
var peers: [EnginePeer.Id: EnginePeer] = [:]
|
var peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||||
peers[peer1.id] = peer1
|
peers[peer1.id] = peer1
|
||||||
@ -3400,6 +3402,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||||
gesture?.cancel()
|
gesture?.cancel()
|
||||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||||
|
}, openStories: { _, _ in
|
||||||
})
|
})
|
||||||
var isInlineMode = false
|
var isInlineMode = false
|
||||||
if case .topics = key {
|
if case .topics = key {
|
||||||
@ -3433,7 +3436,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
associatedMessages: [:],
|
associatedMessages: [:],
|
||||||
associatedMessageIds: [],
|
associatedMessageIds: [],
|
||||||
associatedMedia: [:],
|
associatedMedia: [:],
|
||||||
associatedThreadInfo: nil
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
)
|
)
|
||||||
let readState = EnginePeerReadCounters()
|
let readState = EnginePeerReadCounters()
|
||||||
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
|
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
|
||||||
@ -3453,13 +3457,14 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
hasFailedMessages: false,
|
hasFailedMessages: false,
|
||||||
forumTopicData: nil,
|
forumTopicData: nil,
|
||||||
topForumTopicItems: [],
|
topForumTopicItems: [],
|
||||||
autoremoveTimeout: nil
|
autoremoveTimeout: nil,
|
||||||
|
storyState: nil
|
||||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||||
case .media:
|
case .media:
|
||||||
return nil
|
return nil
|
||||||
case .links:
|
case .links:
|
||||||
var media: [EngineMedia] = []
|
var media: [EngineMedia] = []
|
||||||
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, attributes: [], instantPage: nil)))))
|
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))))
|
||||||
let message = EngineMessage(
|
let message = EngineMessage(
|
||||||
stableId: 0,
|
stableId: 0,
|
||||||
stableVersion: 0,
|
stableVersion: 0,
|
||||||
@ -3482,7 +3487,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
associatedMessages: [:],
|
associatedMessages: [:],
|
||||||
associatedMessageIds: [],
|
associatedMessageIds: [],
|
||||||
associatedMedia: [:],
|
associatedMedia: [:],
|
||||||
associatedThreadInfo: nil
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true)
|
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true)
|
||||||
@ -3511,7 +3517,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
associatedMessages: [:],
|
associatedMessages: [:],
|
||||||
associatedMessageIds: [],
|
associatedMessageIds: [],
|
||||||
associatedMedia: [:],
|
associatedMedia: [:],
|
||||||
associatedThreadInfo: nil
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
||||||
@ -3540,7 +3547,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
associatedMessages: [:],
|
associatedMessages: [:],
|
||||||
associatedMessageIds: [],
|
associatedMessageIds: [],
|
||||||
associatedMedia: [:],
|
associatedMedia: [:],
|
||||||
associatedThreadInfo: nil
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
||||||
@ -3569,7 +3577,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
associatedMessages: [:],
|
associatedMessages: [:],
|
||||||
associatedMessageIds: [],
|
associatedMessageIds: [],
|
||||||
associatedMedia: [:],
|
associatedMedia: [:],
|
||||||
associatedThreadInfo: nil
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
|
||||||
|
@ -257,7 +257,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if let duration = file.duration {
|
if let duration = file.duration {
|
||||||
let durationString = stringForDuration(duration)
|
let durationString = stringForDuration(Int32(duration))
|
||||||
|
|
||||||
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
|
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
|
||||||
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
|
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ struct ChatListSelectionOptions: Equatable {
|
|||||||
let delete: Bool
|
let delete: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatListSelectionOptions(context: AccountContext, peerIds: Set<PeerId>, filterId: Int32?) -> Signal<ChatListSelectionOptions, NoError> {
|
func chatListSelectionOptions(context: AccountContext, peerIds: Set<EnginePeer.Id>, filterId: Int32?) -> Signal<ChatListSelectionOptions, NoError> {
|
||||||
if peerIds.isEmpty {
|
if peerIds.isEmpty {
|
||||||
if let filterId = filterId {
|
if let filterId = filterId {
|
||||||
return chatListFilterItems(context: context)
|
return chatListFilterItems(context: context)
|
||||||
@ -58,7 +57,7 @@ func chatListSelectionOptions(context: AccountContext, peerIds: Set<PeerId>, fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func forumSelectionOptions(context: AccountContext, peerId: PeerId, threadIds: Set<Int64>) -> Signal<ChatListSelectionOptions, NoError> {
|
func forumSelectionOptions(context: AccountContext, peerId: EnginePeer.Id, threadIds: Set<Int64>) -> Signal<ChatListSelectionOptions, NoError> {
|
||||||
return context.engine.data.get(
|
return context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||||
EngineDataList(threadIds.map { TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: $0) })
|
EngineDataList(threadIds.map { TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: $0) })
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -64,6 +64,19 @@ public enum ChatListItemContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StoryState: Equatable {
|
||||||
|
public var stats: EngineChatList.StoryStats
|
||||||
|
public var hasUnseenCloseFriends: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
stats: EngineChatList.StoryStats,
|
||||||
|
hasUnseenCloseFriends: Bool
|
||||||
|
) {
|
||||||
|
self.stats = stats
|
||||||
|
self.hasUnseenCloseFriends = hasUnseenCloseFriends
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct PeerData {
|
public struct PeerData {
|
||||||
public var messages: [EngineMessage]
|
public var messages: [EngineMessage]
|
||||||
public var peer: EngineRenderedPeer
|
public var peer: EngineRenderedPeer
|
||||||
@ -82,6 +95,7 @@ public enum ChatListItemContent {
|
|||||||
public var forumTopicData: EngineChatList.ForumTopicData?
|
public var forumTopicData: EngineChatList.ForumTopicData?
|
||||||
public var topForumTopicItems: [EngineChatList.ForumTopicData]
|
public var topForumTopicItems: [EngineChatList.ForumTopicData]
|
||||||
public var autoremoveTimeout: Int32?
|
public var autoremoveTimeout: Int32?
|
||||||
|
public var storyState: StoryState?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
messages: [EngineMessage],
|
messages: [EngineMessage],
|
||||||
@ -100,7 +114,8 @@ public enum ChatListItemContent {
|
|||||||
hasFailedMessages: Bool,
|
hasFailedMessages: Bool,
|
||||||
forumTopicData: EngineChatList.ForumTopicData?,
|
forumTopicData: EngineChatList.ForumTopicData?,
|
||||||
topForumTopicItems: [EngineChatList.ForumTopicData],
|
topForumTopicItems: [EngineChatList.ForumTopicData],
|
||||||
autoremoveTimeout: Int32?
|
autoremoveTimeout: Int32?,
|
||||||
|
storyState: StoryState?
|
||||||
) {
|
) {
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
@ -119,11 +134,37 @@ public enum ChatListItemContent {
|
|||||||
self.forumTopicData = forumTopicData
|
self.forumTopicData = forumTopicData
|
||||||
self.topForumTopicItems = topForumTopicItems
|
self.topForumTopicItems = topForumTopicItems
|
||||||
self.autoremoveTimeout = autoremoveTimeout
|
self.autoremoveTimeout = autoremoveTimeout
|
||||||
|
self.storyState = storyState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GroupReferenceData {
|
||||||
|
public var groupId: EngineChatList.Group
|
||||||
|
public var peers: [EngineChatList.GroupItem.Item]
|
||||||
|
public var message: EngineMessage?
|
||||||
|
public var unreadCount: Int
|
||||||
|
public var hiddenByDefault: Bool
|
||||||
|
public var storyState: StoryState?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
groupId: EngineChatList.Group,
|
||||||
|
peers: [EngineChatList.GroupItem.Item],
|
||||||
|
message: EngineMessage?,
|
||||||
|
unreadCount: Int,
|
||||||
|
hiddenByDefault: Bool,
|
||||||
|
storyState: StoryState?
|
||||||
|
) {
|
||||||
|
self.groupId = groupId
|
||||||
|
self.peers = peers
|
||||||
|
self.message = message
|
||||||
|
self.unreadCount = unreadCount
|
||||||
|
self.hiddenByDefault = hiddenByDefault
|
||||||
|
self.storyState = storyState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case peer(PeerData)
|
case peer(PeerData)
|
||||||
case groupReference(groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, unreadCount: Int, hiddenByDefault: Bool)
|
case groupReference(GroupReferenceData)
|
||||||
|
|
||||||
public var chatLocation: ChatLocation? {
|
public var chatLocation: ChatLocation? {
|
||||||
switch self {
|
switch self {
|
||||||
@ -250,8 +291,8 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour {
|
|||||||
} else if let peer = peerData.peer.peers[peerData.peer.peerId] {
|
} else if let peer = peerData.peer.peers[peerData.peer.peerId] {
|
||||||
self.interaction.peerSelected(peer, nil, nil, peerData.promoInfo)
|
self.interaction.peerSelected(peer, nil, nil, peerData.promoInfo)
|
||||||
}
|
}
|
||||||
case let .groupReference(groupId, _, _, _, _):
|
case let .groupReference(groupReferenceData):
|
||||||
self.interaction.groupSelected(groupId)
|
self.interaction.groupSelected(groupReferenceData.groupId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,6 +940,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var avatarIconView: ComponentHostView<Empty>?
|
var avatarIconView: ComponentHostView<Empty>?
|
||||||
var avatarIconComponent: EmojiStatusComponent?
|
var avatarIconComponent: EmojiStatusComponent?
|
||||||
var avatarVideoNode: AvatarVideoNode?
|
var avatarVideoNode: AvatarVideoNode?
|
||||||
|
var avatarTapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
private var inlineNavigationMarkLayer: SimpleLayer?
|
private var inlineNavigationMarkLayer: SimpleLayer?
|
||||||
|
|
||||||
@ -987,9 +1029,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch item.content {
|
switch item.content {
|
||||||
case let .groupReference(_, _, _, unreadCount, _):
|
case let .groupReference(groupReferenceData):
|
||||||
var result = item.presentationData.strings.ChatList_ArchivedChatsTitle
|
var result = item.presentationData.strings.ChatList_ArchivedChatsTitle
|
||||||
let allCount = unreadCount
|
let allCount = groupReferenceData.unreadCount
|
||||||
if allCount > 0 {
|
if allCount > 0 {
|
||||||
result += "\n\(item.presentationData.strings.VoiceOver_Chat_UnreadMessages(Int32(allCount)))"
|
result += "\n\(item.presentationData.strings.VoiceOver_Chat_UnreadMessages(Int32(allCount)))"
|
||||||
}
|
}
|
||||||
@ -1019,7 +1061,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch item.content {
|
switch item.content {
|
||||||
case let .groupReference(_, peers, messageValue, _, _):
|
case let .groupReference(groupReferenceData):
|
||||||
|
let peers = groupReferenceData.peers
|
||||||
|
let messageValue = groupReferenceData.message
|
||||||
if let message = messageValue, let peer = peers.first?.peer {
|
if let message = messageValue, let peer = peers.first?.peer {
|
||||||
let messages = [message]
|
let messages = [message]
|
||||||
var result = ""
|
var result = ""
|
||||||
@ -1265,6 +1309,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil)
|
item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.onDidLoad { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let avatarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.avatarStoryTapGesture(_:)))
|
||||||
|
self.avatarTapRecognizer = avatarTapRecognizer
|
||||||
|
self.avatarNode.view.addGestureRecognizer(avatarTapRecognizer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -1282,28 +1335,48 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let previousItem = self.item
|
let previousItem = self.item
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
|
var storyState: ChatListItemContent.StoryState?
|
||||||
|
if case let .peer(peerData) = item.content {
|
||||||
|
storyState = peerData.storyState
|
||||||
|
} else if case let .groupReference(groupReference) = item.content {
|
||||||
|
storyState = groupReference.storyState
|
||||||
|
}
|
||||||
|
|
||||||
var peer: EnginePeer?
|
var peer: EnginePeer?
|
||||||
var displayAsMessage = false
|
var displayAsMessage = false
|
||||||
var enablePreview = true
|
var enablePreview = true
|
||||||
switch item.content {
|
switch item.content {
|
||||||
case let .peer(peerData):
|
case let .peer(peerData):
|
||||||
displayAsMessage = peerData.displayAsMessage
|
displayAsMessage = peerData.displayAsMessage
|
||||||
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
|
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
|
||||||
peer = .user(author)
|
peer = .user(author)
|
||||||
} else {
|
} else {
|
||||||
peer = peerData.peer.chatMainPeer
|
peer = peerData.peer.chatMainPeer
|
||||||
}
|
}
|
||||||
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||||
enablePreview = false
|
enablePreview = false
|
||||||
}
|
}
|
||||||
case let .groupReference(_, _, _, _, hiddenByDefault):
|
case let .groupReference(groupReferenceData):
|
||||||
if let previousItem = previousItem, case let .groupReference(_, _, _, _, previousHiddenByDefault) = previousItem.content, hiddenByDefault != previousHiddenByDefault {
|
if let previousItem = previousItem, case let .groupReference(previousGroupReferenceData) = previousItem.content, groupReferenceData.hiddenByDefault != previousGroupReferenceData.hiddenByDefault {
|
||||||
UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
|
UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
|
||||||
}, completion: nil)
|
}, completion: nil)
|
||||||
}
|
}
|
||||||
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
|
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in
|
||||||
|
return AvatarNode.StoryStats(
|
||||||
|
totalCount: storyState.stats.totalCount,
|
||||||
|
unseenCount: storyState.stats.unseenCount,
|
||||||
|
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}, presentationParams: AvatarNode.StoryPresentationParams(
|
||||||
|
colors: AvatarNode.Colors(theme: item.presentationData.theme),
|
||||||
|
lineWidth: 2.33,
|
||||||
|
inactiveLineWidth: 1.33
|
||||||
|
), transition: .immediate)
|
||||||
|
self.avatarNode.isUserInteractionEnabled = storyState != nil
|
||||||
|
|
||||||
if let peer = peer {
|
if let peer = peer {
|
||||||
var overrideImage: AvatarNodeImageOverride?
|
var overrideImage: AvatarNodeImageOverride?
|
||||||
if peer.id.isReplies {
|
if peer.id.isReplies {
|
||||||
@ -1350,7 +1423,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
videoNode = current
|
videoNode = current
|
||||||
} else {
|
} else {
|
||||||
videoNode = AvatarVideoNode(context: item.context)
|
videoNode = AvatarVideoNode(context: item.context)
|
||||||
strongSelf.avatarNode.addSubnode(videoNode)
|
strongSelf.avatarNode.contentNode.addSubnode(videoNode)
|
||||||
strongSelf.avatarVideoNode = videoNode
|
strongSelf.avatarVideoNode = videoNode
|
||||||
}
|
}
|
||||||
videoNode.update(peer: peer, photo: photo, size: CGSize(width: 60.0, height: 60.0))
|
videoNode.update(peer: peer, photo: photo, size: CGSize(width: 60.0, height: 60.0))
|
||||||
@ -1610,7 +1683,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
promoInfo = promoInfoValue
|
promoInfo = promoInfoValue
|
||||||
displayAsMessage = displayAsMessageValue
|
displayAsMessage = displayAsMessageValue
|
||||||
hasFailedMessages = messagesValue.last?.flags.contains(.Failed) ?? false // hasFailedMessagesValue
|
hasFailedMessages = messagesValue.last?.flags.contains(.Failed) ?? false // hasFailedMessagesValue
|
||||||
case let .groupReference(_, peers, messageValue, unreadCountValue, hiddenByDefault):
|
case let .groupReference(groupReferenceData):
|
||||||
|
let peers = groupReferenceData.peers
|
||||||
|
let messageValue = groupReferenceData.message
|
||||||
|
let unreadCountValue = groupReferenceData.unreadCount
|
||||||
|
let hiddenByDefault = groupReferenceData.hiddenByDefault
|
||||||
|
|
||||||
if let _ = messageValue, !peers.isEmpty {
|
if let _ = messageValue, !peers.isEmpty {
|
||||||
contentPeer = .chat(peers[0].peer)
|
contentPeer = .chat(peers[0].peer)
|
||||||
} else {
|
} else {
|
||||||
@ -1679,6 +1757,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var currentCredibilityIconContent: EmojiStatusComponent.Content?
|
var currentCredibilityIconContent: EmojiStatusComponent.Content?
|
||||||
var currentSecretIconImage: UIImage?
|
var currentSecretIconImage: UIImage?
|
||||||
var currentForwardedIcon: UIImage?
|
var currentForwardedIcon: UIImage?
|
||||||
|
var currentStoryIcon: UIImage?
|
||||||
|
|
||||||
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
||||||
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
|
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
|
||||||
@ -1801,6 +1880,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)?
|
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)?
|
||||||
|
|
||||||
var displayForwardedIcon = false
|
var displayForwardedIcon = false
|
||||||
|
var displayStoryReplyIcon = false
|
||||||
|
|
||||||
switch contentData {
|
switch contentData {
|
||||||
case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges):
|
case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges):
|
||||||
@ -1979,6 +2059,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) {
|
if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) {
|
||||||
displayForwardedIcon = true
|
displayForwardedIcon = true
|
||||||
|
} else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) {
|
||||||
|
displayStoryReplyIcon = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayMediaPreviews = true
|
var displayMediaPreviews = true
|
||||||
@ -2025,6 +2107,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image {
|
} else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image {
|
||||||
let fitSize = contentImageSize
|
let fitSize = contentImageSize
|
||||||
contentImageSpecs.append((message, .action(action), fitSize))
|
contentImageSpecs.append((message, .action(action), fitSize))
|
||||||
|
} else if let storyMedia = media as? TelegramMediaStory, let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self) {
|
||||||
|
if let image = storyItem.media as? TelegramMediaImage {
|
||||||
|
if let _ = largestImageRepresentation(image.representations) {
|
||||||
|
let fitSize = contentImageSize
|
||||||
|
contentImageSpecs.append((message, .image(image), fitSize))
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
} else if let file = storyItem.media as? TelegramMediaFile {
|
||||||
|
if file.isVideo, !file.isInstantVideo, let _ = file.dimensions {
|
||||||
|
let fitSize = contentImageSize
|
||||||
|
contentImageSpecs.append((message, .file(file), fitSize))
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2059,6 +2155,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if textString.length == 0, case let .groupReference(data) = item.content, let storyState = data.storyState, storyState.stats.totalCount != 0 {
|
||||||
|
let storyText: String = item.presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyState.stats.totalCount))
|
||||||
|
textString.append(NSAttributedString(string: storyText, font: textFont, textColor: theme.messageTextColor))
|
||||||
|
}
|
||||||
attributedText = textString
|
attributedText = textString
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2066,6 +2166,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme)
|
currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if displayStoryReplyIcon {
|
||||||
|
currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme)
|
||||||
|
}
|
||||||
|
|
||||||
if let currentForwardedIcon {
|
if let currentForwardedIcon {
|
||||||
textLeftCutout += currentForwardedIcon.size.width
|
textLeftCutout += currentForwardedIcon.size.width
|
||||||
if !contentImageSpecs.isEmpty {
|
if !contentImageSpecs.isEmpty {
|
||||||
@ -2075,6 +2179,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let currentStoryIcon {
|
||||||
|
textLeftCutout += currentStoryIcon.size.width
|
||||||
|
if !contentImageSpecs.isEmpty {
|
||||||
|
textLeftCutout += forwardedIconSpacing
|
||||||
|
} else {
|
||||||
|
textLeftCutout += contentImageTrailingSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i in 0 ..< contentImageSpecs.count {
|
for i in 0 ..< contentImageSpecs.count {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
textLeftCutout += contentImageSpacing
|
textLeftCutout += contentImageSpacing
|
||||||
@ -2115,8 +2228,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let dateText: String
|
let dateText: String
|
||||||
var topIndex: MessageIndex?
|
var topIndex: MessageIndex?
|
||||||
switch item.content {
|
switch item.content {
|
||||||
case let .groupReference(_, _, message, _, _):
|
case let .groupReference(groupReferenceData):
|
||||||
topIndex = message?.index
|
topIndex = groupReferenceData.message?.index
|
||||||
case let .peer(peerData):
|
case let .peer(peerData):
|
||||||
topIndex = peerData.messages.first?.index
|
topIndex = peerData.messages.first?.index
|
||||||
}
|
}
|
||||||
@ -2735,6 +2848,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let targetAvatarScaleOffset: CGFloat = -(avatarFrame.width - avatarFrame.width * avatarScale) * 0.5
|
let targetAvatarScaleOffset: CGFloat = -(avatarFrame.width - avatarFrame.width * avatarScale) * 0.5
|
||||||
avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress
|
avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame)
|
transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame)
|
||||||
transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0))
|
transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0))
|
||||||
transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
@ -3273,15 +3387,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
inputActivitiesApply?()
|
inputActivitiesApply?()
|
||||||
|
|
||||||
var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: floor((measureLayout.size.height - contentImageSize.height) / 2.0))
|
var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: 1.0 + floor((measureLayout.size.height - contentImageSize.height) / 2.0))
|
||||||
|
|
||||||
if let currentForwardedIcon = currentForwardedIcon {
|
var messageTypeIcon: UIImage?
|
||||||
strongSelf.forwardedIconNode.image = currentForwardedIcon
|
var messageTypeIconOffset = mediaPreviewOffset
|
||||||
|
if let currentForwardedIcon {
|
||||||
|
messageTypeIcon = currentForwardedIcon
|
||||||
|
messageTypeIconOffset.y += 3.0
|
||||||
|
} else if let currentStoryIcon {
|
||||||
|
messageTypeIcon = currentStoryIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
if let messageTypeIcon {
|
||||||
|
strongSelf.forwardedIconNode.image = messageTypeIcon
|
||||||
if strongSelf.forwardedIconNode.supernode == nil {
|
if strongSelf.forwardedIconNode.supernode == nil {
|
||||||
strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode)
|
strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode)
|
||||||
}
|
}
|
||||||
transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: CGPoint(x: mediaPreviewOffset.x, y: mediaPreviewOffset.y + 3.0), size: currentForwardedIcon.size))
|
transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIcon.size))
|
||||||
mediaPreviewOffset.x += currentForwardedIcon.size.width + forwardedIconSpacing
|
mediaPreviewOffset.x += messageTypeIcon.size.width + forwardedIconSpacing
|
||||||
} else if strongSelf.forwardedIconNode.supernode != nil {
|
} else if strongSelf.forwardedIconNode.supernode != nil {
|
||||||
strongSelf.forwardedIconNode.removeFromSupernode()
|
strongSelf.forwardedIconNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
@ -3403,7 +3526,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let separatorInset: CGFloat
|
let separatorInset: CGFloat
|
||||||
if case let .groupReference(_, _, _, _, hiddenByDefault) = item.content, hiddenByDefault {
|
if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault {
|
||||||
separatorInset = 0.0
|
separatorInset = 0.0
|
||||||
} else if (!nextIsPinned && isPinned) || last {
|
} else if (!nextIsPinned && isPinned) || last {
|
||||||
separatorInset = 0.0
|
separatorInset = 0.0
|
||||||
@ -3425,7 +3548,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
backgroundColor = theme.itemSelectedBackgroundColor
|
backgroundColor = theme.itemSelectedBackgroundColor
|
||||||
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
|
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
|
||||||
} else if isPinned {
|
} else if isPinned {
|
||||||
if case let .groupReference(_, _, _, _, hiddenByDefault) = item.content, hiddenByDefault {
|
if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault {
|
||||||
backgroundColor = theme.itemBackgroundColor
|
backgroundColor = theme.itemBackgroundColor
|
||||||
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
|
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
|
||||||
} else {
|
} else {
|
||||||
@ -3742,6 +3865,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let item = self.item else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if let compoundTextButtonNode = self.compoundTextButtonNode, let compoundHighlightingNode = self.compoundHighlightingNode, compoundHighlightingNode.alpha != 0.0 {
|
if let compoundTextButtonNode = self.compoundTextButtonNode, let compoundHighlightingNode = self.compoundHighlightingNode, compoundHighlightingNode.alpha != 0.0 {
|
||||||
let localPoint = self.view.convert(point, to: compoundHighlightingNode.view)
|
let localPoint = self.view.convert(point, to: compoundHighlightingNode.view)
|
||||||
var matches = false
|
var matches = false
|
||||||
@ -3756,6 +3883,29 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let _ = item.interaction.inlineNavigationLocation {
|
||||||
|
} else {
|
||||||
|
if self.avatarNode.storyStats != nil {
|
||||||
|
if let result = self.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarNode.view), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.hitTest(point, with: event)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func avatarStoryTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
guard let item = self.item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch item.content {
|
||||||
|
case let .peer(peerData):
|
||||||
|
item.interaction.openStories(.peer(peerData.peer.peerId), self)
|
||||||
|
case .groupReference:
|
||||||
|
item.interaction.openStories(.archive, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
processed = true
|
processed = true
|
||||||
break inner
|
break inner
|
||||||
}
|
}
|
||||||
case let .Video(_, _, flags):
|
case let .Video(_, _, flags, _):
|
||||||
if flags.contains(.instantRoundVideo) {
|
if flags.contains(.instantRoundVideo) {
|
||||||
messageText = strings.Message_VideoMessage
|
messageText = strings.Message_VideoMessage
|
||||||
processed = true
|
processed = true
|
||||||
@ -295,6 +295,16 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
messageText = "📊 \(poll.text)"
|
messageText = "📊 \(poll.text)"
|
||||||
case let dice as TelegramMediaDice:
|
case let dice as TelegramMediaDice:
|
||||||
messageText = dice.emoji
|
messageText = dice.emoji
|
||||||
|
case let story as TelegramMediaStory:
|
||||||
|
if story.isMention, let peer {
|
||||||
|
if message.flags.contains(.Incoming) {
|
||||||
|
messageText = strings.Conversation_StoryMentionTextIncoming(peer.compactDisplayTitle).string
|
||||||
|
} else {
|
||||||
|
messageText = strings.Conversation_StoryMentionTextOutgoing(peer.compactDisplayTitle).string
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageText = strings.Notification_Story
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,12 @@ import AnimationCache
|
|||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
import Postbox
|
import Postbox
|
||||||
import ChatFolderLinkPreviewScreen
|
import ChatFolderLinkPreviewScreen
|
||||||
|
import StoryContainerScreen
|
||||||
|
import ChatListHeaderComponent
|
||||||
|
|
||||||
public enum ChatListNodeMode {
|
public enum ChatListNodeMode {
|
||||||
case chatList(appendContacts: Bool)
|
case chatList(appendContacts: Bool)
|
||||||
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
|
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool)
|
||||||
case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool)
|
case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +100,7 @@ public final class ChatListNodeInteraction {
|
|||||||
let openPremiumIntro: () -> Void
|
let openPremiumIntro: () -> Void
|
||||||
let openChatFolderUpdates: () -> Void
|
let openChatFolderUpdates: () -> Void
|
||||||
let hideChatFolderUpdates: () -> Void
|
let hideChatFolderUpdates: () -> Void
|
||||||
|
let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void
|
||||||
|
|
||||||
public var searchTextHighightState: String?
|
public var searchTextHighightState: String?
|
||||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||||
@ -144,7 +147,8 @@ public final class ChatListNodeInteraction {
|
|||||||
openPasswordSetup: @escaping () -> Void,
|
openPasswordSetup: @escaping () -> Void,
|
||||||
openPremiumIntro: @escaping () -> Void,
|
openPremiumIntro: @escaping () -> Void,
|
||||||
openChatFolderUpdates: @escaping () -> Void,
|
openChatFolderUpdates: @escaping () -> Void,
|
||||||
hideChatFolderUpdates: @escaping () -> Void
|
hideChatFolderUpdates: @escaping () -> Void,
|
||||||
|
openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void
|
||||||
) {
|
) {
|
||||||
self.activateSearch = activateSearch
|
self.activateSearch = activateSearch
|
||||||
self.peerSelected = peerSelected
|
self.peerSelected = peerSelected
|
||||||
@ -179,6 +183,7 @@ public final class ChatListNodeInteraction {
|
|||||||
self.openPremiumIntro = openPremiumIntro
|
self.openPremiumIntro = openPremiumIntro
|
||||||
self.openChatFolderUpdates = openChatFolderUpdates
|
self.openChatFolderUpdates = openChatFolderUpdates
|
||||||
self.hideChatFolderUpdates = hideChatFolderUpdates
|
self.hideChatFolderUpdates = hideChatFolderUpdates
|
||||||
|
self.openStories = openStories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,6 +218,16 @@ private func areFoundPeerArraysEqual(_ lhs: [(EnginePeer, EnginePeer?)], _ rhs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatListNodeState: Equatable {
|
public struct ChatListNodeState: Equatable {
|
||||||
|
public struct StoryState: Equatable {
|
||||||
|
public var stats: EngineChatList.StoryStats
|
||||||
|
public var hasUnseenCloseFriends: Bool
|
||||||
|
|
||||||
|
public init(stats: EngineChatList.StoryStats, hasUnseenCloseFriends: Bool) {
|
||||||
|
self.stats = stats
|
||||||
|
self.hasUnseenCloseFriends = hasUnseenCloseFriends
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ItemId: Hashable {
|
public struct ItemId: Hashable {
|
||||||
public var peerId: EnginePeer.Id
|
public var peerId: EnginePeer.Id
|
||||||
public var threadId: Int64?
|
public var threadId: Int64?
|
||||||
@ -236,6 +251,7 @@ public struct ChatListNodeState: Equatable {
|
|||||||
public var foundPeers: [(EnginePeer, EnginePeer?)]
|
public var foundPeers: [(EnginePeer, EnginePeer?)]
|
||||||
public var selectedPeerMap: [EnginePeer.Id: EnginePeer]
|
public var selectedPeerMap: [EnginePeer.Id: EnginePeer]
|
||||||
public var selectedThreadIds: Set<Int64>
|
public var selectedThreadIds: Set<Int64>
|
||||||
|
public var archiveStoryState: StoryState?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
presentationData: ChatListPresentationData,
|
presentationData: ChatListPresentationData,
|
||||||
@ -250,7 +266,8 @@ public struct ChatListNodeState: Equatable {
|
|||||||
pendingClearHistoryPeerIds: Set<ItemId>,
|
pendingClearHistoryPeerIds: Set<ItemId>,
|
||||||
hiddenItemShouldBeTemporaryRevealed: Bool,
|
hiddenItemShouldBeTemporaryRevealed: Bool,
|
||||||
hiddenPsaPeerId: EnginePeer.Id?,
|
hiddenPsaPeerId: EnginePeer.Id?,
|
||||||
selectedThreadIds: Set<Int64>
|
selectedThreadIds: Set<Int64>,
|
||||||
|
archiveStoryState: StoryState?
|
||||||
) {
|
) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.editing = editing
|
self.editing = editing
|
||||||
@ -265,6 +282,7 @@ public struct ChatListNodeState: Equatable {
|
|||||||
self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed
|
self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed
|
||||||
self.hiddenPsaPeerId = hiddenPsaPeerId
|
self.hiddenPsaPeerId = hiddenPsaPeerId
|
||||||
self.selectedThreadIds = selectedThreadIds
|
self.selectedThreadIds = selectedThreadIds
|
||||||
|
self.archiveStoryState = archiveStoryState
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool {
|
public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool {
|
||||||
@ -307,6 +325,9 @@ public struct ChatListNodeState: Equatable {
|
|||||||
if lhs.selectedThreadIds != rhs.selectedThreadIds {
|
if lhs.selectedThreadIds != rhs.selectedThreadIds {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.archiveStoryState != rhs.archiveStoryState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,7 +405,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hasFailedMessages: hasFailedMessages,
|
hasFailedMessages: hasFailedMessages,
|
||||||
forumTopicData: forumTopicData,
|
forumTopicData: forumTopicData,
|
||||||
topForumTopicItems: topForumTopicItems,
|
topForumTopicItems: topForumTopicItems,
|
||||||
autoremoveTimeout: peerEntry.autoremoveTimeout
|
autoremoveTimeout: peerEntry.autoremoveTimeout,
|
||||||
|
storyState: peerEntry.storyState.flatMap { storyState in
|
||||||
|
return ChatListItemContent.StoryState(
|
||||||
|
stats: storyState.stats,
|
||||||
|
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}
|
||||||
)),
|
)),
|
||||||
editing: editing,
|
editing: editing,
|
||||||
hasActiveRevealControls: hasActiveRevealControls,
|
hasActiveRevealControls: hasActiveRevealControls,
|
||||||
@ -394,7 +421,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hiddenOffset: threadInfo?.isHidden == true && !revealed,
|
hiddenOffset: threadInfo?.isHidden == true && !revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), directionHint: entry.directionHint)
|
||||||
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
|
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
|
||||||
let itemPeer = peer.chatMainPeer
|
let itemPeer = peer.chatMainPeer
|
||||||
var chatPeer: EnginePeer?
|
var chatPeer: EnginePeer?
|
||||||
if let peer = peer.peers[peer.peerId] {
|
if let peer = peer.peers[peer.peerId] {
|
||||||
@ -477,7 +504,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
|
|
||||||
var header: ChatListSearchItemHeader?
|
var header: ChatListSearchItemHeader?
|
||||||
switch mode {
|
switch mode {
|
||||||
case let .peers(_, _, additionalCategories, _, _):
|
case let .peers(_, _, additionalCategories, _, _, _):
|
||||||
if !additionalCategories.isEmpty {
|
if !additionalCategories.isEmpty {
|
||||||
let headerType: ChatListSearchItemHeaderType
|
let headerType: ChatListSearchItemHeaderType
|
||||||
if case .action = additionalCategories[0].appearance {
|
if case .action = additionalCategories[0].appearance {
|
||||||
@ -494,7 +521,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
|
|
||||||
var status: ContactsPeerItemStatus = .none
|
var status: ContactsPeerItemStatus = .none
|
||||||
if isSelecting, let itemPeer = itemPeer {
|
if isSelecting, let itemPeer = itemPeer {
|
||||||
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
|
if displayPresence, let presence = presence {
|
||||||
|
status = .presence(presence, presentationData.dateTimeFormat)
|
||||||
|
} else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
|
||||||
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
|
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
|
||||||
} else {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
@ -594,26 +623,32 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
}
|
}
|
||||||
case let .HoleEntry(_, theme):
|
case let .HoleEntry(_, theme):
|
||||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
|
||||||
case let .GroupReferenceEntry(index, presentationData, groupId, peers, message, editing, unreadCount, revealed, hiddenByDefault):
|
case let .GroupReferenceEntry(groupReferenceEntry):
|
||||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
|
||||||
presentationData: presentationData,
|
presentationData: groupReferenceEntry.presentationData,
|
||||||
context: context,
|
context: context,
|
||||||
chatListLocation: location,
|
chatListLocation: location,
|
||||||
filterData: filterData,
|
filterData: filterData,
|
||||||
index: index,
|
index: groupReferenceEntry.index,
|
||||||
content: .groupReference(
|
content: .groupReference(ChatListItemContent.GroupReferenceData(
|
||||||
groupId: groupId,
|
groupId: groupReferenceEntry.groupId,
|
||||||
peers: peers,
|
peers: groupReferenceEntry.peers,
|
||||||
message: message,
|
message: groupReferenceEntry.message,
|
||||||
unreadCount: unreadCount,
|
unreadCount: groupReferenceEntry.unreadCount,
|
||||||
hiddenByDefault: hiddenByDefault
|
hiddenByDefault: groupReferenceEntry.hiddenByDefault,
|
||||||
),
|
storyState: groupReferenceEntry.storyState.flatMap { storyState in
|
||||||
editing: editing,
|
return ChatListItemContent.StoryState(
|
||||||
|
stats: storyState.stats,
|
||||||
|
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
editing: groupReferenceEntry.editing,
|
||||||
hasActiveRevealControls: false,
|
hasActiveRevealControls: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
header: nil,
|
header: nil,
|
||||||
enableContextActions: true,
|
enableContextActions: true,
|
||||||
hiddenOffset: hiddenByDefault && !revealed,
|
hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), directionHint: entry.directionHint)
|
||||||
case let .ContactEntry(contactEntry):
|
case let .ContactEntry(contactEntry):
|
||||||
@ -665,13 +700,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
nodeInteraction?.openPasswordSetup()
|
nodeInteraction?.openPasswordSetup()
|
||||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||||
nodeInteraction?.openPremiumIntro()
|
nodeInteraction?.openPremiumIntro()
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.openChatFolderUpdates()
|
|
||||||
}
|
}
|
||||||
case .hide:
|
case .hide:
|
||||||
switch notice {
|
switch notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.hideChatFolderUpdates()
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -731,7 +762,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hasFailedMessages: hasFailedMessages,
|
hasFailedMessages: hasFailedMessages,
|
||||||
forumTopicData: forumTopicData,
|
forumTopicData: forumTopicData,
|
||||||
topForumTopicItems: topForumTopicItems,
|
topForumTopicItems: topForumTopicItems,
|
||||||
autoremoveTimeout: peerEntry.autoremoveTimeout
|
autoremoveTimeout: peerEntry.autoremoveTimeout,
|
||||||
|
storyState: peerEntry.storyState.flatMap { storyState in
|
||||||
|
return ChatListItemContent.StoryState(
|
||||||
|
stats: storyState.stats,
|
||||||
|
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}
|
||||||
)),
|
)),
|
||||||
editing: editing,
|
editing: editing,
|
||||||
hasActiveRevealControls: hasActiveRevealControls,
|
hasActiveRevealControls: hasActiveRevealControls,
|
||||||
@ -741,7 +778,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hiddenOffset: threadInfo?.isHidden == true && !revealed,
|
hiddenOffset: threadInfo?.isHidden == true && !revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), directionHint: entry.directionHint)
|
||||||
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
|
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
|
||||||
let itemPeer = peer.chatMainPeer
|
let itemPeer = peer.chatMainPeer
|
||||||
var chatPeer: EnginePeer?
|
var chatPeer: EnginePeer?
|
||||||
if let peer = peer.peers[peer.peerId] {
|
if let peer = peer.peers[peer.peerId] {
|
||||||
@ -778,7 +815,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
|
|
||||||
var header: ChatListSearchItemHeader?
|
var header: ChatListSearchItemHeader?
|
||||||
switch mode {
|
switch mode {
|
||||||
case let .peers(_, _, additionalCategories, _, _):
|
case let .peers(_, _, additionalCategories, _, _, _):
|
||||||
if !additionalCategories.isEmpty {
|
if !additionalCategories.isEmpty {
|
||||||
let headerType: ChatListSearchItemHeaderType
|
let headerType: ChatListSearchItemHeaderType
|
||||||
if case .action = additionalCategories[0].appearance {
|
if case .action = additionalCategories[0].appearance {
|
||||||
@ -795,7 +832,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
|
|
||||||
var status: ContactsPeerItemStatus = .none
|
var status: ContactsPeerItemStatus = .none
|
||||||
if isSelecting, let itemPeer = itemPeer {
|
if isSelecting, let itemPeer = itemPeer {
|
||||||
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
|
if displayPresence, let presence = presence {
|
||||||
|
status = .presence(presence, presentationData.dateTimeFormat)
|
||||||
|
} else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
|
||||||
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
|
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
|
||||||
} else {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
@ -895,26 +934,32 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
}
|
}
|
||||||
case let .HoleEntry(_, theme):
|
case let .HoleEntry(_, theme):
|
||||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
|
||||||
case let .GroupReferenceEntry(index, presentationData, groupId, peers, message, editing, unreadCount, revealed, hiddenByDefault):
|
case let .GroupReferenceEntry(groupReferenceEntry):
|
||||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
|
||||||
presentationData: presentationData,
|
presentationData: groupReferenceEntry.presentationData,
|
||||||
context: context,
|
context: context,
|
||||||
chatListLocation: location,
|
chatListLocation: location,
|
||||||
filterData: filterData,
|
filterData: filterData,
|
||||||
index: index,
|
index: groupReferenceEntry.index,
|
||||||
content: .groupReference(
|
content: .groupReference(ChatListItemContent.GroupReferenceData(
|
||||||
groupId: groupId,
|
groupId: groupReferenceEntry.groupId,
|
||||||
peers: peers,
|
peers: groupReferenceEntry.peers,
|
||||||
message: message,
|
message: groupReferenceEntry.message,
|
||||||
unreadCount: unreadCount,
|
unreadCount: groupReferenceEntry.unreadCount,
|
||||||
hiddenByDefault: hiddenByDefault
|
hiddenByDefault: groupReferenceEntry.hiddenByDefault,
|
||||||
),
|
storyState: groupReferenceEntry.storyState.flatMap { storyState in
|
||||||
editing: editing,
|
return ChatListItemContent.StoryState(
|
||||||
|
stats: storyState.stats,
|
||||||
|
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
editing: groupReferenceEntry.editing,
|
||||||
hasActiveRevealControls: false,
|
hasActiveRevealControls: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
header: nil,
|
header: nil,
|
||||||
enableContextActions: true,
|
enableContextActions: true,
|
||||||
hiddenOffset: hiddenByDefault && !revealed,
|
hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), directionHint: entry.directionHint)
|
||||||
case let .ContactEntry(contactEntry):
|
case let .ContactEntry(contactEntry):
|
||||||
@ -966,13 +1011,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
nodeInteraction?.openPasswordSetup()
|
nodeInteraction?.openPasswordSetup()
|
||||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||||
nodeInteraction?.openPremiumIntro()
|
nodeInteraction?.openPremiumIntro()
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.openChatFolderUpdates()
|
|
||||||
}
|
}
|
||||||
case .hide:
|
case .hide:
|
||||||
switch notice {
|
switch notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.hideChatFolderUpdates()
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -1031,7 +1072,7 @@ public enum ChatListGlobalScrollOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum ChatListNodeScrollPosition {
|
public enum ChatListNodeScrollPosition {
|
||||||
case top
|
case top(adjustForTempInset: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChatListNodeEmptyState: Equatable {
|
public enum ChatListNodeEmptyState: Equatable {
|
||||||
@ -1040,6 +1081,11 @@ public enum ChatListNodeEmptyState: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ChatListNode: ListView {
|
public final class ChatListNode: ListView {
|
||||||
|
public enum OpenStoriesSubject {
|
||||||
|
case peer(EnginePeer.Id)
|
||||||
|
case archive
|
||||||
|
}
|
||||||
|
|
||||||
private let fillPreloadItems: Bool
|
private let fillPreloadItems: Bool
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let location: ChatListControllerLocation
|
private let location: ChatListControllerLocation
|
||||||
@ -1077,6 +1123,7 @@ public final class ChatListNode: ListView {
|
|||||||
public var toggleArchivedFolderHiddenByDefault: (() -> Void)?
|
public var toggleArchivedFolderHiddenByDefault: (() -> Void)?
|
||||||
public var hidePsa: ((EnginePeer.Id) -> Void)?
|
public var hidePsa: ((EnginePeer.Id) -> Void)?
|
||||||
public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||||
|
public var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)?
|
||||||
|
|
||||||
private var theme: PresentationTheme
|
private var theme: PresentationTheme
|
||||||
|
|
||||||
@ -1141,10 +1188,13 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
|
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
|
||||||
public var contentScrollingEnded: ((ListView) -> Bool)?
|
public var contentScrollingEnded: ((ListView) -> Bool)?
|
||||||
|
public var didBeginInteractiveDragging: ((ListView) -> Void)?
|
||||||
|
|
||||||
public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
|
public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
|
||||||
private var currentIsEmptyState: ChatListNodeEmptyState?
|
private var currentIsEmptyState: ChatListNodeEmptyState?
|
||||||
|
|
||||||
|
public var canExpandHiddenItems: (() -> Bool)?
|
||||||
|
|
||||||
public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
|
public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
|
||||||
|
|
||||||
private let currentRemovingItemId = Atomic<ChatListNodeState.ItemId?>(value: nil)
|
private let currentRemovingItemId = Atomic<ChatListNodeState.ItemId?>(value: nil)
|
||||||
@ -1175,7 +1225,17 @@ public final class ChatListNode: ListView {
|
|||||||
private var pollFilterUpdatesDisposable: Disposable?
|
private var pollFilterUpdatesDisposable: Disposable?
|
||||||
private var chatFilterUpdatesDisposable: Disposable?
|
private var chatFilterUpdatesDisposable: Disposable?
|
||||||
|
|
||||||
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
|
public var scrollHeightTopInset: CGFloat {
|
||||||
|
didSet {
|
||||||
|
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var startedScrollingAtUpperBound: Bool = false
|
||||||
|
|
||||||
|
private let autoSetReady: Bool
|
||||||
|
|
||||||
|
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.location = location
|
self.location = location
|
||||||
self.chatListFilter = chatListFilter
|
self.chatListFilter = chatListFilter
|
||||||
@ -1184,23 +1244,30 @@ public final class ChatListNode: ListView {
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.animationCache = animationCache
|
self.animationCache = animationCache
|
||||||
self.animationRenderer = animationRenderer
|
self.animationRenderer = animationRenderer
|
||||||
|
self.autoSetReady = autoSetReady
|
||||||
|
|
||||||
|
let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root)
|
||||||
|
|
||||||
var isSelecting = false
|
var isSelecting = false
|
||||||
if case .peers(_, true, _, _, _) = mode {
|
if case .peers(_, true, _, _, _, _) = mode {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set())
|
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), archiveStoryState: nil)
|
||||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||||
|
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
|
self.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
//self.useMainQueueTransactions = true
|
||||||
|
|
||||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||||
|
|
||||||
self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight
|
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
|
||||||
|
|
||||||
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
|
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
|
||||||
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
|
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
|
||||||
@ -1525,6 +1592,11 @@ public final class ChatListNode: ListView {
|
|||||||
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start()
|
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}, openStories: { [weak self] subject, itemNode in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openStories?(subject, itemNode)
|
||||||
})
|
})
|
||||||
nodeInteraction.isInlineMode = isInlineMode
|
nodeInteraction.isInlineMode = isInlineMode
|
||||||
|
|
||||||
@ -1545,7 +1617,7 @@ public final class ChatListNode: ListView {
|
|||||||
let currentRemovingItemId = self.currentRemovingItemId
|
let currentRemovingItemId = self.currentRemovingItemId
|
||||||
|
|
||||||
let savedMessagesPeer: Signal<EnginePeer?, NoError>
|
let savedMessagesPeer: Signal<EnginePeer?, NoError>
|
||||||
if case let .peers(filter, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
|
if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
|
||||||
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> map { peer in
|
|> map { peer in
|
||||||
@ -1778,7 +1850,7 @@ public final class ChatListNode: ListView {
|
|||||||
})*/
|
})*/
|
||||||
|
|
||||||
let contacts: Signal<[ChatListContactPeer], NoError>
|
let contacts: Signal<[ChatListContactPeer], NoError>
|
||||||
if case .chatList(groupId: .root) = location, chatListFilter == nil {
|
if case .chatList(groupId: .root) = location, chatListFilter == nil, case .chatList = mode {
|
||||||
contacts = ApplicationSpecificNotice.displayChatListContacts(accountManager: context.sharedContext.accountManager)
|
contacts = ApplicationSpecificNotice.displayChatListContacts(accountManager: context.sharedContext.accountManager)
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|> mapToSignal { value -> Signal<[ChatListContactPeer], NoError> in
|
|> mapToSignal { value -> Signal<[ChatListContactPeer], NoError> in
|
||||||
@ -1836,6 +1908,8 @@ public final class ChatListNode: ListView {
|
|||||||
contacts = .single([])
|
contacts = .single([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accountPeerId = context.account.peerId
|
||||||
|
|
||||||
let chatListNodeViewTransition = combineLatest(
|
let chatListNodeViewTransition = combineLatest(
|
||||||
queue: viewProcessingQueue,
|
queue: viewProcessingQueue,
|
||||||
hideArchivedFolderByDefault,
|
hideArchivedFolderByDefault,
|
||||||
@ -1844,19 +1918,16 @@ public final class ChatListNode: ListView {
|
|||||||
suggestedChatListNotice,
|
suggestedChatListNotice,
|
||||||
savedMessagesPeer,
|
savedMessagesPeer,
|
||||||
chatListViewUpdate,
|
chatListViewUpdate,
|
||||||
self.chatFolderUpdates.get() |> distinctUntilChanged,
|
|
||||||
self.statePromise.get(),
|
self.statePromise.get(),
|
||||||
contacts
|
contacts
|
||||||
)
|
)
|
||||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
|
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||||
let (update, filter) = updateAndFilter
|
let (update, filter) = updateAndFilter
|
||||||
|
|
||||||
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
||||||
|
|
||||||
let notice: ChatListNotice?
|
let notice: ChatListNotice?
|
||||||
if let chatFolderUpdates, chatFolderUpdates.availableChatsToJoin != 0 {
|
if let suggestedChatListNotice {
|
||||||
notice = .chatFolderUpdates(count: chatFolderUpdates.availableChatsToJoin)
|
|
||||||
} else if let suggestedChatListNotice {
|
|
||||||
notice = suggestedChatListNotice
|
notice = suggestedChatListNotice
|
||||||
} else if let storageInfo {
|
} else if let storageInfo {
|
||||||
notice = .clearStorage(sizeFraction: storageInfo)
|
notice = .clearStorage(sizeFraction: storageInfo)
|
||||||
@ -1864,7 +1935,7 @@ public final class ChatListNode: ListView {
|
|||||||
notice = nil
|
notice = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts)
|
let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: isMainTab)
|
||||||
var isEmpty = true
|
var isEmpty = true
|
||||||
var entries = rawEntries.filter { entry in
|
var entries = rawEntries.filter { entry in
|
||||||
switch entry {
|
switch entry {
|
||||||
@ -1875,7 +1946,7 @@ public final class ChatListNode: ListView {
|
|||||||
case .chatList:
|
case .chatList:
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
return true
|
return true
|
||||||
case let .peers(filter, _, _, _, _):
|
case let .peers(filter, _, _, _, _, _):
|
||||||
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
|
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
|
||||||
guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false }
|
guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false }
|
||||||
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
|
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
|
||||||
@ -2210,8 +2281,8 @@ public final class ChatListNode: ListView {
|
|||||||
didIncludeRemovingPeerId = true
|
didIncludeRemovingPeerId = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry {
|
} else if case let .GroupReferenceEntry(groupReferenceEntry) = entry {
|
||||||
didIncludeHiddenByDefaultArchive = hiddenByDefault
|
didIncludeHiddenByDefaultArchive = groupReferenceEntry.hiddenByDefault
|
||||||
} else if case .Notice = entry {
|
} else if case .Notice = entry {
|
||||||
didIncludeNotice = true
|
didIncludeNotice = true
|
||||||
}
|
}
|
||||||
@ -2246,9 +2317,9 @@ public final class ChatListNode: ListView {
|
|||||||
doesIncludeRemovingPeerId = true
|
doesIncludeRemovingPeerId = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry {
|
} else if case let .GroupReferenceEntry(groupReferenceEntry) = entry {
|
||||||
doesIncludeArchive = true
|
doesIncludeArchive = true
|
||||||
doesIncludeHiddenByDefaultArchive = hiddenByDefault
|
doesIncludeHiddenByDefaultArchive = groupReferenceEntry.hiddenByDefault
|
||||||
} else if case .Notice = entry {
|
} else if case .Notice = entry {
|
||||||
doesIncludeNotice = true
|
doesIncludeNotice = true
|
||||||
}
|
}
|
||||||
@ -2334,10 +2405,11 @@ public final class ChatListNode: ListView {
|
|||||||
strongSelf.enqueueHistoryPreloadUpdate()
|
strongSelf.enqueueHistoryPreloadUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var refreshStoryPeerIds: [PeerId] = []
|
||||||
var isHiddenItemVisible = false
|
var isHiddenItemVisible = false
|
||||||
if let range = range.visibleRange {
|
if let range = range.visibleRange {
|
||||||
let entryCount = chatListView.filteredEntries.count
|
let entryCount = chatListView.filteredEntries.count
|
||||||
for i in range.firstIndex ..< range.lastIndex {
|
for i in max(0, range.firstIndex - 1) ..< range.lastIndex {
|
||||||
if i < 0 || i >= entryCount {
|
if i < 0 || i >= entryCount {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
continue
|
continue
|
||||||
@ -2349,6 +2421,11 @@ public final class ChatListNode: ListView {
|
|||||||
if let threadInfo, threadInfo.isHidden {
|
if let threadInfo, threadInfo.isHidden {
|
||||||
isHiddenItemVisible = true
|
isHiddenItemVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer {
|
||||||
|
refreshStoryPeerIds.append(user.id)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case .GroupReferenceEntry:
|
case .GroupReferenceEntry:
|
||||||
isHiddenItemVisible = true
|
isHiddenItemVisible = true
|
||||||
@ -2364,6 +2441,9 @@ public final class ChatListNode: ListView {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !refreshStoryPeerIds.isEmpty {
|
||||||
|
strongSelf.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: refreshStoryPeerIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2695,18 +2775,25 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var startedScrollingAtUpperBound = false
|
var startedScrollingWithCanExpandHiddenItems = false
|
||||||
|
|
||||||
self.beganInteractiveDragging = { [weak self] _ in
|
self.beganInteractiveDragging = { [weak self] _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch strongSelf.visibleContentOffset() {
|
switch strongSelf.visibleContentOffset() {
|
||||||
case .none, .unknown:
|
case .none, .unknown:
|
||||||
startedScrollingAtUpperBound = false
|
strongSelf.startedScrollingAtUpperBound = false
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
startedScrollingAtUpperBound = value <= 0.0
|
strongSelf.startedScrollingAtUpperBound = value <= 0.001
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let canExpandHiddenItems = strongSelf.canExpandHiddenItems {
|
||||||
|
startedScrollingWithCanExpandHiddenItems = canExpandHiddenItems()
|
||||||
|
} else {
|
||||||
|
startedScrollingWithCanExpandHiddenItems = true
|
||||||
|
}
|
||||||
|
|
||||||
if strongSelf.currentState.peerIdWithRevealedOptions != nil {
|
if strongSelf.currentState.peerIdWithRevealedOptions != nil {
|
||||||
strongSelf.updateState { state in
|
strongSelf.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
@ -2714,27 +2801,28 @@ public final class ChatListNode: ListView {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.didBeginInteractiveDragging?(strongSelf)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.didEndScrolling = { [weak self] _ in
|
self.didEndScrolling = { [weak self] _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startedScrollingAtUpperBound = false
|
|
||||||
let _ = strongSelf.contentScrollingEnded?(strongSelf)
|
let _ = strongSelf.contentScrollingEnded?(strongSelf)
|
||||||
let revealHiddenItems: Bool
|
let revealHiddenItems: Bool
|
||||||
switch strongSelf.visibleContentOffset() {
|
switch strongSelf.visibleContentOffset() {
|
||||||
case .none, .unknown:
|
case .none, .unknown:
|
||||||
revealHiddenItems = false
|
revealHiddenItems = false
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
revealHiddenItems = value <= 54.0
|
revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0
|
||||||
}
|
}
|
||||||
if !revealHiddenItems && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed {
|
if !revealHiddenItems && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed {
|
||||||
strongSelf.updateState { state in
|
/*strongSelf.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
state.hiddenItemShouldBeTemporaryRevealed = false
|
state.hiddenItemShouldBeTemporaryRevealed = false
|
||||||
return state
|
return state
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2762,9 +2850,9 @@ public final class ChatListNode: ListView {
|
|||||||
case .none, .unknown:
|
case .none, .unknown:
|
||||||
atTop = false
|
atTop = false
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
atTop = value <= 0.0
|
atTop = value <= -strongSelf.tempTopInset
|
||||||
if startedScrollingAtUpperBound && strongSelf.isTracking {
|
if strongSelf.startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking {
|
||||||
revealHiddenItems = value <= -60.0
|
revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.scrolledAtTopValue = atTop
|
strongSelf.scrolledAtTopValue = atTop
|
||||||
@ -2778,8 +2866,8 @@ public final class ChatListNode: ListView {
|
|||||||
isHiddenItemVisible = true
|
isHiddenItemVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if case let .groupReference(_, _, _, _, hiddenByDefault) = item.content {
|
if case let .groupReference(groupReference) = item.content {
|
||||||
if hiddenByDefault {
|
if groupReference.hiddenByDefault {
|
||||||
isHiddenItemVisible = true
|
isHiddenItemVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2906,7 +2994,7 @@ public final class ChatListNode: ListView {
|
|||||||
if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout {
|
if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout {
|
||||||
strongSelf.dequeueTransition()
|
strongSelf.dequeueTransition()
|
||||||
} else {
|
} else {
|
||||||
if !strongSelf.didSetReady {
|
if !strongSelf.didSetReady && strongSelf.autoSetReady {
|
||||||
strongSelf.didSetReady = true
|
strongSelf.didSetReady = true
|
||||||
strongSelf._ready.set(true)
|
strongSelf._ready.set(true)
|
||||||
}
|
}
|
||||||
@ -3109,6 +3197,7 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var options = transition.options
|
var options = transition.options
|
||||||
|
//options.insert(.Synchronous)
|
||||||
if self.view.window != nil {
|
if self.view.window != nil {
|
||||||
if !options.contains(.AnimateInsertion) {
|
if !options.contains(.AnimateInsertion) {
|
||||||
options.insert(.PreferSynchronousDrawing)
|
options.insert(.PreferSynchronousDrawing)
|
||||||
@ -3129,7 +3218,7 @@ public final class ChatListNode: ListView {
|
|||||||
case let .known(value) where abs(value) < .ulpOfOne:
|
case let .known(value) where abs(value) < .ulpOfOne:
|
||||||
offset = 0.0
|
offset = 0.0
|
||||||
default:
|
default:
|
||||||
offset = -navigationBarSearchContentHeight
|
offset = -self.scrollHeightTopInset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||||
@ -3142,7 +3231,7 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
var isNavigationHidden: Bool {
|
var isNavigationHidden: Bool {
|
||||||
switch self.visibleContentOffset() {
|
switch self.visibleContentOffset() {
|
||||||
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
|
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
|
||||||
return false
|
return false
|
||||||
case .none:
|
case .none:
|
||||||
return false
|
return false
|
||||||
@ -3154,17 +3243,18 @@ public final class ChatListNode: ListView {
|
|||||||
var isNavigationInAFinalState: Bool {
|
var isNavigationInAFinalState: Bool {
|
||||||
switch self.visibleContentOffset() {
|
switch self.visibleContentOffset() {
|
||||||
case let .known(value):
|
case let .known(value):
|
||||||
if value < navigationBarSearchContentHeight - 1.0 {
|
let _ = value
|
||||||
|
/*if value < self.scrollHeightTopInset - 1.0 {
|
||||||
if abs(value - 0.0) < 1.0 {
|
if abs(value - 0.0) < 1.0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if abs(value - navigationBarSearchContentHeight) < 1.0 {
|
if abs(value - self.scrollHeightTopInset) < 1.0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {*/
|
||||||
return true
|
return true
|
||||||
}
|
//}
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -3176,9 +3266,9 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
var scrollToItem: ListViewScrollToItem?
|
var scrollToItem: ListViewScrollToItem?
|
||||||
switch self.visibleContentOffset() {
|
switch self.visibleContentOffset() {
|
||||||
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
|
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
|
||||||
if isNavigationHidden {
|
if isNavigationHidden {
|
||||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-self.scrollHeightTopInset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if !isNavigationHidden {
|
if !isNavigationHidden {
|
||||||
@ -3197,7 +3287,11 @@ public final class ChatListNode: ListView {
|
|||||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })*/
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, visibleTopInset: CGFloat, originalTopInset: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat) {
|
public var ignoreStoryInsetAdjustment: Bool = false
|
||||||
|
private var previousStoriesInset: CGFloat?
|
||||||
|
|
||||||
|
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, visibleTopInset: CGFloat, originalTopInset: CGFloat, storiesInset: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat) {
|
||||||
|
//print("inset: \(updateSizeAndInsets.insets.top)")
|
||||||
|
|
||||||
var highlightedLocation: ChatListHighlightedLocation?
|
var highlightedLocation: ChatListHighlightedLocation?
|
||||||
if case let .forum(peerId) = inlineNavigationLocation {
|
if case let .forum(peerId) = inlineNavigationLocation {
|
||||||
@ -3234,6 +3328,23 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
var additionalScrollDistance: CGFloat = 0.0
|
var additionalScrollDistance: CGFloat = 0.0
|
||||||
|
|
||||||
|
if let previousStoriesInset = self.previousStoriesInset {
|
||||||
|
if self.ignoreStoryInsetAdjustment {
|
||||||
|
//additionalScrollDistance += -20.0
|
||||||
|
switch self.visibleContentOffset() {
|
||||||
|
case let .known(value):
|
||||||
|
additionalScrollDistance += min(0.0, value)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
additionalScrollDistance = 0.0
|
||||||
|
} else {
|
||||||
|
additionalScrollDistance += previousStoriesInset - storiesInset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.previousStoriesInset = storiesInset
|
||||||
|
//print("storiesInset: \(storiesInset), additionalScrollDistance: \(additionalScrollDistance)")
|
||||||
|
|
||||||
var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency]
|
var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency]
|
||||||
if navigationLocationUpdated {
|
if navigationLocationUpdated {
|
||||||
options.insert(.ForceUpdate)
|
options.insert(.ForceUpdate)
|
||||||
@ -3244,7 +3355,9 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
additionalScrollDistance += insetDelta
|
additionalScrollDistance += insetDelta
|
||||||
}
|
}
|
||||||
|
self.ignoreStopScrolling = true
|
||||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: options, scrollToItem: nil, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: options, scrollToItem: nil, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||||
|
self.ignoreStopScrolling = false
|
||||||
|
|
||||||
if !self.dequeuedInitialTransitionOnLayout {
|
if !self.dequeuedInitialTransitionOnLayout {
|
||||||
self.dequeuedInitialTransitionOnLayout = true
|
self.dequeuedInitialTransitionOnLayout = true
|
||||||
@ -3253,16 +3366,25 @@ public final class ChatListNode: ListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrollToPosition(_ position: ChatListNodeScrollPosition) {
|
public func scrollToPosition(_ position: ChatListNodeScrollPosition, animated: Bool = true) {
|
||||||
|
var additionalDelta: CGFloat = 0.0
|
||||||
|
switch position {
|
||||||
|
case let .top(adjustForTempInset):
|
||||||
|
if adjustForTempInset {
|
||||||
|
additionalDelta = ChatListNavigationBar.storiesScrollHeight
|
||||||
|
self.tempTopInset = ChatListNavigationBar.storiesScrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let list = self.chatListView?.originalList {
|
if let list = self.chatListView?.originalList {
|
||||||
if !list.hasLater {
|
if !list.hasLater {
|
||||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(additionalDelta), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||||
} else {
|
} else {
|
||||||
let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter)
|
let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter)
|
||||||
self.setChatListLocation(location)
|
self.setChatListLocation(location)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter)
|
let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter)
|
||||||
self.setChatListLocation(location)
|
self.setChatListLocation(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3714,10 +3836,12 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
|
|||||||
} else if case let .user(user) = peer {
|
} else if case let .user(user) = peer {
|
||||||
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
||||||
return (strings.ChatList_PeerTypeBot, false, false, nil)
|
return (strings.ChatList_PeerTypeBot, false, false, nil)
|
||||||
} else if isContact {
|
|
||||||
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
|
||||||
} else {
|
} else {
|
||||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
if isContact {
|
||||||
|
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
||||||
|
} else {
|
||||||
|
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if case .secretChat = peer {
|
} else if case .secretChat = peer {
|
||||||
if isContact {
|
if isContact {
|
||||||
|
@ -112,6 +112,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
var forumTopicData: EngineChatList.ForumTopicData?
|
var forumTopicData: EngineChatList.ForumTopicData?
|
||||||
var topForumTopicItems: [EngineChatList.ForumTopicData]
|
var topForumTopicItems: [EngineChatList.ForumTopicData]
|
||||||
var revealed: Bool
|
var revealed: Bool
|
||||||
|
var storyState: ChatListNodeState.StoryState?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
index: EngineChatList.Item.Index,
|
index: EngineChatList.Item.Index,
|
||||||
@ -135,7 +136,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
autoremoveTimeout: Int32?,
|
autoremoveTimeout: Int32?,
|
||||||
forumTopicData: EngineChatList.ForumTopicData?,
|
forumTopicData: EngineChatList.ForumTopicData?,
|
||||||
topForumTopicItems: [EngineChatList.ForumTopicData],
|
topForumTopicItems: [EngineChatList.ForumTopicData],
|
||||||
revealed: Bool
|
revealed: Bool,
|
||||||
|
storyState: ChatListNodeState.StoryState?
|
||||||
) {
|
) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
@ -159,6 +161,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
self.forumTopicData = forumTopicData
|
self.forumTopicData = forumTopicData
|
||||||
self.topForumTopicItems = topForumTopicItems
|
self.topForumTopicItems = topForumTopicItems
|
||||||
self.revealed = revealed
|
self.revealed = revealed
|
||||||
|
self.storyState = storyState
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
|
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
|
||||||
@ -268,6 +271,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
if lhs.revealed != rhs.revealed {
|
if lhs.revealed != rhs.revealed {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.storyState != rhs.storyState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,10 +303,82 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GroupReferenceEntryData: Equatable {
|
||||||
|
var index: EngineChatList.Item.Index
|
||||||
|
var presentationData: ChatListPresentationData
|
||||||
|
var groupId: EngineChatList.Group
|
||||||
|
var peers: [EngineChatList.GroupItem.Item]
|
||||||
|
var message: EngineMessage?
|
||||||
|
var editing: Bool
|
||||||
|
var unreadCount: Int
|
||||||
|
var revealed: Bool
|
||||||
|
var hiddenByDefault: Bool
|
||||||
|
var storyState: ChatListNodeState.StoryState?
|
||||||
|
|
||||||
|
init(
|
||||||
|
index: EngineChatList.Item.Index,
|
||||||
|
presentationData: ChatListPresentationData,
|
||||||
|
groupId: EngineChatList.Group,
|
||||||
|
peers: [EngineChatList.GroupItem.Item],
|
||||||
|
message: EngineMessage?,
|
||||||
|
editing: Bool,
|
||||||
|
unreadCount: Int,
|
||||||
|
revealed: Bool,
|
||||||
|
hiddenByDefault: Bool,
|
||||||
|
storyState: ChatListNodeState.StoryState?
|
||||||
|
) {
|
||||||
|
self.index = index
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.groupId = groupId
|
||||||
|
self.peers = peers
|
||||||
|
self.message = message
|
||||||
|
self.editing = editing
|
||||||
|
self.unreadCount = unreadCount
|
||||||
|
self.revealed = revealed
|
||||||
|
self.hiddenByDefault = hiddenByDefault
|
||||||
|
self.storyState = storyState
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: GroupReferenceEntryData, rhs: GroupReferenceEntryData) -> Bool {
|
||||||
|
if lhs.index != rhs.index {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.presentationData !== rhs.presentationData {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.groupId != rhs.groupId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peers != rhs.peers {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.message?.stableId != rhs.message?.stableId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.editing != rhs.editing {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.unreadCount != rhs.unreadCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.revealed != rhs.revealed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hiddenByDefault != rhs.hiddenByDefault {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.storyState != rhs.storyState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case HeaderEntry
|
case HeaderEntry
|
||||||
case PeerEntry(PeerEntryData)
|
case PeerEntry(PeerEntryData)
|
||||||
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
|
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
|
||||||
case GroupReferenceEntry(index: EngineChatList.Item.Index, presentationData: ChatListPresentationData, groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, editing: Bool, unreadCount: Int, revealed: Bool, hiddenByDefault: Bool)
|
case GroupReferenceEntry(GroupReferenceEntryData)
|
||||||
case ContactEntry(ContactEntryData)
|
case ContactEntry(ContactEntryData)
|
||||||
case ArchiveIntro(presentationData: ChatListPresentationData)
|
case ArchiveIntro(presentationData: ChatListPresentationData)
|
||||||
case EmptyIntro(presentationData: ChatListPresentationData)
|
case EmptyIntro(presentationData: ChatListPresentationData)
|
||||||
@ -316,8 +394,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
return .index(peerEntry.index)
|
return .index(peerEntry.index)
|
||||||
case let .HoleEntry(holeIndex, _):
|
case let .HoleEntry(holeIndex, _):
|
||||||
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
|
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
|
||||||
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
|
case let .GroupReferenceEntry(groupReferenceEntry):
|
||||||
return .index(index)
|
return .index(groupReferenceEntry.index)
|
||||||
case let .ContactEntry(contactEntry):
|
case let .ContactEntry(contactEntry):
|
||||||
return .contact(id: contactEntry.peer.id, presence: contactEntry.presence)
|
return .contact(id: contactEntry.peer.id, presence: contactEntry.presence)
|
||||||
case .ArchiveIntro:
|
case .ArchiveIntro:
|
||||||
@ -346,8 +424,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
}
|
}
|
||||||
case let .HoleEntry(holeIndex, _):
|
case let .HoleEntry(holeIndex, _):
|
||||||
return .Hole(Int64(holeIndex.id.id))
|
return .Hole(Int64(holeIndex.id.id))
|
||||||
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
|
case let .GroupReferenceEntry(groupReferenceEntry):
|
||||||
return .GroupId(groupId)
|
return .GroupId(groupReferenceEntry.groupId)
|
||||||
case let .ContactEntry(contactEntry):
|
case let .ContactEntry(contactEntry):
|
||||||
return .ContactId(contactEntry.peer.id)
|
return .ContactId(contactEntry.peer.id)
|
||||||
case .ArchiveIntro:
|
case .ArchiveIntro:
|
||||||
@ -388,35 +466,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .GroupReferenceEntry(lhsIndex, lhsPresentationData, lhsGroupId, lhsPeers, lhsMessage, lhsEditing, lhsUnreadState, lhsRevealed, lhsHiddenByDefault):
|
case let .GroupReferenceEntry(groupReferenceEntry):
|
||||||
if case let .GroupReferenceEntry(rhsIndex, rhsPresentationData, rhsGroupId, rhsPeers, rhsMessage, rhsEditing, rhsUnreadState, rhsRevealed, rhsHiddenByDefault) = rhs {
|
if case .GroupReferenceEntry(groupReferenceEntry) = rhs {
|
||||||
if lhsIndex != rhsIndex {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsPresentationData !== rhsPresentationData {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsGroupId != rhsGroupId {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsPeers != rhsPeers {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsMessage?.stableId != rhsMessage?.stableId {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsEditing != rhsEditing {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsUnreadState != rhsUnreadState {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsRevealed != rhsRevealed {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhsHiddenByDefault != rhsHiddenByDefault {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -518,7 +569,17 @@ struct ChatListContactPeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer]) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
||||||
|
var groupItems = view.groupItems
|
||||||
|
if isMainTab && state.archiveStoryState != nil && groupItems.isEmpty {
|
||||||
|
groupItems.append(EngineChatList.GroupItem(
|
||||||
|
id: .archive,
|
||||||
|
topMessage: nil,
|
||||||
|
items: [],
|
||||||
|
unreadCount: 0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
var result: [ChatListNodeEntry] = []
|
var result: [ChatListNodeEntry] = []
|
||||||
|
|
||||||
if !view.hasEarlier {
|
if !view.hasEarlier {
|
||||||
@ -538,7 +599,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
|
|
||||||
if !view.hasLater, case .chatList = mode {
|
if !view.hasLater, case .chatList = mode {
|
||||||
var groupEntryCount = 0
|
var groupEntryCount = 0
|
||||||
for _ in view.groupItems {
|
for _ in groupItems {
|
||||||
groupEntryCount += 1
|
groupEntryCount += 1
|
||||||
}
|
}
|
||||||
pinnedIndexOffset += UInt16(groupEntryCount)
|
pinnedIndexOffset += UInt16(groupEntryCount)
|
||||||
@ -633,7 +694,13 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
autoremoveTimeout: entry.autoremoveTimeout,
|
autoremoveTimeout: entry.autoremoveTimeout,
|
||||||
forumTopicData: entry.forumTopicData,
|
forumTopicData: entry.forumTopicData,
|
||||||
topForumTopicItems: entry.topForumTopicItems,
|
topForumTopicItems: entry.topForumTopicItems,
|
||||||
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing)
|
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing),
|
||||||
|
storyState: entry.renderedPeer.peerId == accountPeerId ? nil : entry.storyStats.flatMap { stats -> ChatListNodeState.StoryState in
|
||||||
|
return ChatListNodeState.StoryState(
|
||||||
|
stats: stats,
|
||||||
|
hasUnseenCloseFriends: stats.hasUnseenCloseFriends
|
||||||
|
)
|
||||||
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
if let threadInfo, threadInfo.isHidden {
|
if let threadInfo, threadInfo.isHidden {
|
||||||
@ -682,7 +749,8 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
autoremoveTimeout: nil,
|
autoremoveTimeout: nil,
|
||||||
forumTopicData: nil,
|
forumTopicData: nil,
|
||||||
topForumTopicItems: [],
|
topForumTopicItems: [],
|
||||||
revealed: false
|
revealed: false,
|
||||||
|
storyState: nil
|
||||||
)))
|
)))
|
||||||
if foundPinningIndex != 0 {
|
if foundPinningIndex != 0 {
|
||||||
foundPinningIndex -= 1
|
foundPinningIndex -= 1
|
||||||
@ -712,7 +780,8 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
autoremoveTimeout: nil,
|
autoremoveTimeout: nil,
|
||||||
forumTopicData: nil,
|
forumTopicData: nil,
|
||||||
topForumTopicItems: [],
|
topForumTopicItems: [],
|
||||||
revealed: false
|
revealed: false,
|
||||||
|
storyState: nil
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
if !filteredAdditionalItemEntries.isEmpty {
|
if !filteredAdditionalItemEntries.isEmpty {
|
||||||
@ -762,7 +831,8 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
autoremoveTimeout: item.item.autoremoveTimeout,
|
autoremoveTimeout: item.item.autoremoveTimeout,
|
||||||
forumTopicData: item.item.forumTopicData,
|
forumTopicData: item.item.forumTopicData,
|
||||||
topForumTopicItems: item.item.topForumTopicItems,
|
topForumTopicItems: item.item.topForumTopicItems,
|
||||||
revealed: state.hiddenItemShouldBeTemporaryRevealed || state.editing
|
revealed: state.hiddenItemShouldBeTemporaryRevealed || state.editing,
|
||||||
|
storyState: nil
|
||||||
)))
|
)))
|
||||||
if pinningIndex != 0 {
|
if pinningIndex != 0 {
|
||||||
pinningIndex -= 1
|
pinningIndex -= 1
|
||||||
@ -772,9 +842,13 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !view.hasLater, case .chatList = mode {
|
if !view.hasLater, case .chatList = mode {
|
||||||
for groupReference in view.groupItems {
|
for groupReference in groupItems {
|
||||||
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1)
|
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1)
|
||||||
result.append(.GroupReferenceEntry(
|
var mappedStoryState: ChatListNodeState.StoryState?
|
||||||
|
if let archiveStoryState = state.archiveStoryState {
|
||||||
|
mappedStoryState = archiveStoryState
|
||||||
|
}
|
||||||
|
result.append(.GroupReferenceEntry(ChatListNodeEntry.GroupReferenceEntryData(
|
||||||
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: messageIndex)),
|
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: messageIndex)),
|
||||||
presentationData: state.presentationData,
|
presentationData: state.presentationData,
|
||||||
groupId: groupReference.id,
|
groupId: groupReference.id,
|
||||||
@ -783,15 +857,16 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
editing: state.editing,
|
editing: state.editing,
|
||||||
unreadCount: groupReference.unreadCount,
|
unreadCount: groupReference.unreadCount,
|
||||||
revealed: state.hiddenItemShouldBeTemporaryRevealed,
|
revealed: state.hiddenItemShouldBeTemporaryRevealed,
|
||||||
hiddenByDefault: hideArchivedFolderByDefault
|
hiddenByDefault: hideArchivedFolderByDefault,
|
||||||
))
|
storyState: mappedStoryState
|
||||||
|
)))
|
||||||
if pinningIndex != 0 {
|
if pinningIndex != 0 {
|
||||||
pinningIndex -= 1
|
pinningIndex -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if displayArchiveIntro {
|
if displayArchiveIntro {
|
||||||
result.append(.ArchiveIntro(presentationData: state.presentationData))
|
//result.append(.ArchiveIntro(presentationData: state.presentationData))
|
||||||
} else if !contacts.isEmpty && !result.contains(where: { entry in
|
} else if !contacts.isEmpty && !result.contains(where: { entry in
|
||||||
if case .PeerEntry = entry {
|
if case .PeerEntry = entry {
|
||||||
return true
|
return true
|
||||||
@ -810,7 +885,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !view.hasLater {
|
if !view.hasLater {
|
||||||
if case let .peers(_, _, additionalCategories, _, _) = mode {
|
if case let .peers(_, _, additionalCategories, _, _, _) = mode {
|
||||||
var index = 0
|
var index = 0
|
||||||
for category in additionalCategories.reversed() {
|
for category in additionalCategories.reversed() {
|
||||||
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
||||||
|
@ -288,7 +288,8 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat
|
|||||||
topForumTopicItems: [],
|
topForumTopicItems: [],
|
||||||
hasFailed: false,
|
hasFailed: false,
|
||||||
isContact: false,
|
isContact: false,
|
||||||
autoremoveTimeout: nil
|
autoremoveTimeout: nil,
|
||||||
|
storyStats: nil
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -183,15 +182,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
|||||||
titleString = titleStringValue
|
titleString = titleStringValue
|
||||||
|
|
||||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||||
case let .chatFolderUpdates(count):
|
|
||||||
let rawTitleString = item.strings.ChatList_ChatFolderUpdateHintTitle(item.strings.ChatList_ChatFolderUpdateCount(Int32(count)))
|
|
||||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
|
||||||
if let range = rawTitleString.ranges.first {
|
|
||||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
|
||||||
}
|
|
||||||
titleString = titleStringValue
|
|
||||||
|
|
||||||
textString = NSAttributedString(string: item.strings.ChatList_ChatFolderUpdateHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
|
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
|
||||||
@ -230,8 +220,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||||
|
|
||||||
switch item.notice {
|
switch item.notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.ChatList_HideAction, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
|
||||||
default:
|
default:
|
||||||
strongSelf.setRevealOptions((left: [], right: []))
|
strongSelf.setRevealOptions((left: [], right: []))
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
|
|
||||||
@ -523,7 +522,7 @@ public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
|||||||
self.clipsToBounds = true
|
self.clipsToBounds = true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setMaskMode(_ maskMode: Bool, mediaBox: MediaBox) {
|
public func setMaskMode(_ maskMode: Bool) {
|
||||||
if let currentType = self.currentType, let theme = self.theme, let essentialGraphics = self.essentialGraphics, let backgroundNode = self.backgroundNode {
|
if let currentType = self.currentType, let theme = self.theme, let essentialGraphics = self.essentialGraphics, let backgroundNode = self.backgroundNode {
|
||||||
self.setType(type: currentType, theme: theme, essentialGraphics: essentialGraphics, maskMode: maskMode, backgroundNode: backgroundNode)
|
self.setType(type: currentType, theme: theme, essentialGraphics: essentialGraphics, maskMode: maskMode, backgroundNode: backgroundNode)
|
||||||
}
|
}
|
||||||
@ -685,7 +684,7 @@ public final class ChatMessageBubbleBackdrop: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public func animateFrom(sourceView: UIView, mediaBox: MediaBox, transition: CombinedTransition) {
|
public func animateFrom(sourceView: UIView, transition: CombinedTransition) {
|
||||||
if transition.isAnimated {
|
if transition.isAnimated {
|
||||||
let previousFrame = self.frame
|
let previousFrame = self.frame
|
||||||
self.updateFrame(CGRect(origin: CGPoint(x: previousFrame.minX, y: sourceView.frame.minY), size: sourceView.frame.size), transition: .immediate)
|
self.updateFrame(CGRect(origin: CGPoint(x: previousFrame.minX, y: sourceView.frame.minY), size: sourceView.frame.size), transition: .immediate)
|
||||||
|
@ -19,6 +19,7 @@ swift_library(
|
|||||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
|
"//submodules/ChatContextQuery",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public struct MessageMediaEditingOptions: OptionSet {
|
public struct MessageMediaEditingOptions: OptionSet {
|
||||||
|
@ -6,6 +6,7 @@ import TelegramPresentationData
|
|||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import ChatInterfaceState
|
import ChatInterfaceState
|
||||||
|
import ChatContextQuery
|
||||||
|
|
||||||
public extension ChatLocation {
|
public extension ChatLocation {
|
||||||
var peerId: PeerId? {
|
var peerId: PeerId? {
|
||||||
@ -31,53 +32,6 @@ public extension ChatLocation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChatPresentationInputQueryKind: Int32 {
|
|
||||||
case emoji
|
|
||||||
case hashtag
|
|
||||||
case mention
|
|
||||||
case command
|
|
||||||
case contextRequest
|
|
||||||
case emojiSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ChatInputQueryMentionTypes: OptionSet, Hashable {
|
|
||||||
public var rawValue: Int32
|
|
||||||
|
|
||||||
public init(rawValue: Int32) {
|
|
||||||
self.rawValue = rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let contextBots = ChatInputQueryMentionTypes(rawValue: 1 << 0)
|
|
||||||
public static let members = ChatInputQueryMentionTypes(rawValue: 1 << 1)
|
|
||||||
public static let accountPeer = ChatInputQueryMentionTypes(rawValue: 1 << 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ChatPresentationInputQuery: Hashable, Equatable {
|
|
||||||
case emoji(String)
|
|
||||||
case hashtag(String)
|
|
||||||
case mention(query: String, types: ChatInputQueryMentionTypes)
|
|
||||||
case command(String)
|
|
||||||
case emojiSearch(query: String, languageCode: String, range: NSRange)
|
|
||||||
case contextRequest(addressName: String, query: String)
|
|
||||||
|
|
||||||
public var kind: ChatPresentationInputQueryKind {
|
|
||||||
switch self {
|
|
||||||
case .emoji:
|
|
||||||
return .emoji
|
|
||||||
case .hashtag:
|
|
||||||
return .hashtag
|
|
||||||
case .mention:
|
|
||||||
return .mention
|
|
||||||
case .command:
|
|
||||||
return .command
|
|
||||||
case .contextRequest:
|
|
||||||
return .contextRequest
|
|
||||||
case .emojiSearch:
|
|
||||||
return .emojiSearch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ChatMediaInputMode {
|
public enum ChatMediaInputMode {
|
||||||
case gif
|
case gif
|
||||||
case other
|
case other
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ public func chatTextInputAddLinkAttribute(_ state: ChatTextInputState, selection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: Peer) -> ChatTextInputState {
|
public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: EnginePeer) -> ChatTextInputState {
|
||||||
let inputText = NSMutableAttributedString(attributedString: state.inputText)
|
let inputText = NSMutableAttributedString(attributedString: state.inputText)
|
||||||
|
|
||||||
let range = NSMakeRange(state.selectionRange.startIndex, state.selectionRange.endIndex - state.selectionRange.startIndex)
|
let range = NSMakeRange(state.selectionRange.startIndex, state.selectionRange.endIndex - state.selectionRange.startIndex)
|
||||||
@ -91,9 +90,9 @@ public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer:
|
|||||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||||
|
|
||||||
return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
|
return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||||
} else if !EnginePeer(peer).compactDisplayTitle.isEmpty {
|
} else if !peer.compactDisplayTitle.isEmpty {
|
||||||
let replacementText = NSMutableAttributedString()
|
let replacementText = NSMutableAttributedString()
|
||||||
replacementText.append(NSAttributedString(string: EnginePeer(peer).compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
||||||
replacementText.append(NSAttributedString(string: " "))
|
replacementText.append(NSAttributedString(string: " "))
|
||||||
|
|
||||||
let updatedRange = NSRange(location: range.location , length: range.length)
|
let updatedRange = NSRange(location: range.location , length: range.length)
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
@ -180,7 +180,8 @@ public final class _UpdatedChildComponent {
|
|||||||
var _opacity: CGFloat?
|
var _opacity: CGFloat?
|
||||||
var _cornerRadius: CGFloat?
|
var _cornerRadius: CGFloat?
|
||||||
var _clipsToBounds: Bool?
|
var _clipsToBounds: Bool?
|
||||||
|
var _shadow: Shadow?
|
||||||
|
|
||||||
fileprivate var transitionAppear: Transition.Appear?
|
fileprivate var transitionAppear: Transition.Appear?
|
||||||
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
||||||
fileprivate var transitionDisappear: Transition.Disappear?
|
fileprivate var transitionDisappear: Transition.Disappear?
|
||||||
@ -240,7 +241,7 @@ public final class _UpdatedChildComponent {
|
|||||||
self._position = position
|
self._position = position
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult public func scale(_ scale: CGFloat) -> _UpdatedChildComponent {
|
@discardableResult public func scale(_ scale: CGFloat) -> _UpdatedChildComponent {
|
||||||
self._scale = scale
|
self._scale = scale
|
||||||
return self
|
return self
|
||||||
@ -260,6 +261,11 @@ public final class _UpdatedChildComponent {
|
|||||||
self._clipsToBounds = clipsToBounds
|
self._clipsToBounds = clipsToBounds
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent {
|
||||||
|
self._shadow = shadow
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
||||||
self.gestures.append(gesture)
|
self.gestures.append(gesture)
|
||||||
@ -702,9 +708,20 @@ public extension CombinedComponent {
|
|||||||
} else {
|
} else {
|
||||||
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
||||||
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
||||||
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
||||||
|
if let shadow = updatedChild._shadow {
|
||||||
|
updatedChild.view.layer.shadowColor = shadow.color.withAlphaComponent(1.0).cgColor
|
||||||
|
updatedChild.view.layer.shadowRadius = shadow.radius
|
||||||
|
updatedChild.view.layer.shadowOpacity = Float(shadow.color.alpha)
|
||||||
|
updatedChild.view.layer.shadowOffset = shadow.offset
|
||||||
|
} else {
|
||||||
|
updatedChild.view.layer.shadowColor = nil
|
||||||
|
updatedChild.view.layer.shadowRadius = 0.0
|
||||||
|
updatedChild.view.layer.shadowOpacity = 0.0
|
||||||
|
}
|
||||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
||||||
guard let viewContext = viewContext else {
|
guard let viewContext = viewContext else {
|
||||||
return
|
return
|
||||||
@ -833,3 +850,19 @@ public extension CombinedComponent {
|
|||||||
return ActionSlot<Arguments>()
|
return ActionSlot<Arguments>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Shadow {
|
||||||
|
public let color: UIColor
|
||||||
|
public let radius: CGFloat
|
||||||
|
public let offset: CGSize
|
||||||
|
|
||||||
|
public init(
|
||||||
|
color: UIColor,
|
||||||
|
radius: CGFloat,
|
||||||
|
offset: CGSize
|
||||||
|
) {
|
||||||
|
self.color = color
|
||||||
|
self.radius = radius
|
||||||
|
self.offset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ import Display
|
|||||||
@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
|
@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private extension UIView {
|
public extension UIView {
|
||||||
static var animationDurationFactor: Double {
|
static var animationDurationFactor: Double {
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
return Double(UIAnimationDragCoefficient())
|
return Double(UIAnimationDragCoefficient())
|
||||||
@ -73,6 +73,21 @@ public struct Transition {
|
|||||||
case easeInOut
|
case easeInOut
|
||||||
case spring
|
case spring
|
||||||
case custom(Float, Float, Float, Float)
|
case custom(Float, Float, Float, Float)
|
||||||
|
|
||||||
|
public func solve(at offset: CGFloat) -> CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .easeInOut:
|
||||||
|
return listViewAnimationCurveEaseInOut(offset)
|
||||||
|
case .spring:
|
||||||
|
return listViewAnimationCurveSystem(offset)
|
||||||
|
case let .custom(c1x, c1y, c2x, c2y):
|
||||||
|
return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var slide: Curve {
|
||||||
|
return .custom(0.33, 0.52, 0.25, 0.99)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case none
|
case none
|
||||||
@ -197,6 +212,62 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setFrameWithAdditivePosition(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
assert(view.layer.anchorPoint == CGPoint())
|
||||||
|
|
||||||
|
if view.frame == frame {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedBounds: Bool?
|
||||||
|
var completedPosition: Bool?
|
||||||
|
let processCompletion: () -> Void = {
|
||||||
|
guard let completedBounds, let completedPosition else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion?(completedBounds && completedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setBounds(view: view, bounds: CGRect(origin: view.bounds.origin, size: frame.size), completion: { value in
|
||||||
|
completedBounds = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
self.animatePosition(view: view, from: CGPoint(x: -frame.minX + view.layer.position.x, y: -frame.minY + view.layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||||
|
completedPosition = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
view.layer.position = frame.origin
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setFrameWithAdditivePosition(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
assert(layer.anchorPoint == CGPoint())
|
||||||
|
|
||||||
|
if layer.frame == frame {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedBounds: Bool?
|
||||||
|
var completedPosition: Bool?
|
||||||
|
let processCompletion: () -> Void = {
|
||||||
|
guard let completedBounds, let completedPosition else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion?(completedBounds && completedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setBounds(layer: layer, bounds: CGRect(origin: layer.bounds.origin, size: frame.size), completion: { value in
|
||||||
|
completedBounds = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
self.animatePosition(layer: layer, from: CGPoint(x: -frame.minX + layer.position.x, y: -frame.minY + layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||||
|
completedPosition = value
|
||||||
|
processCompletion()
|
||||||
|
})
|
||||||
|
layer.position = frame.origin
|
||||||
|
}
|
||||||
|
|
||||||
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
|
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||||
if view.bounds == bounds {
|
if view.bounds == bounds {
|
||||||
completion?(true)
|
completion?(true)
|
||||||
@ -351,7 +422,7 @@ public struct Transition {
|
|||||||
delay: 0.0,
|
delay: 0.0,
|
||||||
curve: curve,
|
curve: curve,
|
||||||
removeOnCompletion: true,
|
removeOnCompletion: true,
|
||||||
additive: true,
|
additive: false,
|
||||||
completion: completion
|
completion: completion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -386,8 +457,15 @@ public struct Transition {
|
|||||||
let t = layer.presentation()?.transform ?? layer.transform
|
let t = layer.presentation()?.transform ?? layer.transform
|
||||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||||
if currentScale == scale {
|
if currentScale == scale {
|
||||||
completion?(true)
|
if let animation = layer.animation(forKey: "transform.scale") as? CABasicAnimation, let toValue = animation.toValue as? NSNumber {
|
||||||
return
|
if toValue.doubleValue == scale {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
@ -414,9 +492,40 @@ public struct Transition {
|
|||||||
self.setTransform(layer: view.layer, transform: transform, completion: completion)
|
self.setTransform(layer: view.layer, transform: transform, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setTransformAsKeyframes(view: UIView, transform: (CGFloat, Bool) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
self.setTransformAsKeyframes(layer: view.layer, transform: transform, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
public func setTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
public func setTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
if let animation = layer.animation(forKey: "transform") as? CABasicAnimation, let toValue = animation.toValue as? NSValue {
|
||||||
|
if CATransform3DEqualToTransform(toValue.caTransform3DValue, transform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if let animation = layer.animation(forKey: "transform") as? CAKeyframeAnimation, let toValue = animation.values?.last as? NSValue {
|
||||||
|
if CATransform3DEqualToTransform(toValue.caTransform3DValue, transform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CATransform3DEqualToTransform(layer.transform, transform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
|
if layer.animation(forKey: "transform") != nil {
|
||||||
|
if let animation = layer.animation(forKey: "transform") as? CAKeyframeAnimation, let toValue = animation.values?.last as? NSValue {
|
||||||
|
if CATransform3DEqualToTransform(toValue.caTransform3DValue, transform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.removeAnimation(forKey: "transform")
|
||||||
|
}
|
||||||
layer.transform = transform
|
layer.transform = transform
|
||||||
completion?(true)
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
@ -426,6 +535,7 @@ public struct Transition {
|
|||||||
} else {
|
} else {
|
||||||
previousValue = layer.transform
|
previousValue = layer.transform
|
||||||
}
|
}
|
||||||
|
|
||||||
layer.transform = transform
|
layer.transform = transform
|
||||||
layer.animate(
|
layer.animate(
|
||||||
from: NSValue(caTransform3D: previousValue),
|
from: NSValue(caTransform3D: previousValue),
|
||||||
@ -441,6 +551,67 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setTransformAsKeyframes(layer: CALayer, transform: (CGFloat, Bool) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
let finalTransform = transform(1.0, true)
|
||||||
|
|
||||||
|
let t = layer.transform
|
||||||
|
do {
|
||||||
|
if let animation = layer.animation(forKey: "transform") as? CABasicAnimation, let toValue = animation.toValue as? NSValue {
|
||||||
|
if CATransform3DEqualToTransform(toValue.caTransform3DValue, finalTransform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if let animation = layer.animation(forKey: "transform") as? CAKeyframeAnimation, let toValue = animation.values?.last as? NSValue {
|
||||||
|
if CATransform3DEqualToTransform(toValue.caTransform3DValue, finalTransform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if CATransform3DEqualToTransform(t, finalTransform) {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
if layer.animation(forKey: "transform") != nil {
|
||||||
|
layer.removeAnimation(forKey: "transform")
|
||||||
|
}
|
||||||
|
layer.transform = transform(1.0, true)
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let framesPerSecond: CGFloat
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
framesPerSecond = duration * CGFloat(UIScreen.main.maximumFramesPerSecond)
|
||||||
|
} else {
|
||||||
|
framesPerSecond = 60.0
|
||||||
|
}
|
||||||
|
|
||||||
|
let numValues = Int(framesPerSecond * duration)
|
||||||
|
if numValues == 0 {
|
||||||
|
layer.transform = transform(1.0, true)
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var values: [AnyObject] = []
|
||||||
|
|
||||||
|
for i in 0 ... numValues {
|
||||||
|
let t = curve.solve(at: CGFloat(i) / CGFloat(numValues))
|
||||||
|
values.append(NSValue(caTransform3D: transform(t, false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.transform = transform(1.0, true)
|
||||||
|
layer.animateKeyframes(
|
||||||
|
values: values,
|
||||||
|
duration: duration,
|
||||||
|
keyPath: "transform",
|
||||||
|
removeOnCompletion: true,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||||
self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion)
|
self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion)
|
||||||
}
|
}
|
||||||
@ -732,6 +903,80 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setShapeLayerStrokeStart(layer: CAShapeLayer, strokeStart: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
layer.strokeStart = strokeStart
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let previousStrokeStart = layer.strokeStart
|
||||||
|
layer.strokeStart = strokeStart
|
||||||
|
|
||||||
|
layer.animate(
|
||||||
|
from: previousStrokeStart as NSNumber,
|
||||||
|
to: strokeStart as NSNumber,
|
||||||
|
keyPath: "strokeStart",
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
curve: curve,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setShapeLayerStrokeEnd(layer: CAShapeLayer, strokeEnd: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
layer.strokeEnd = strokeEnd
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let previousStrokeEnd = layer.strokeEnd
|
||||||
|
layer.strokeEnd = strokeEnd
|
||||||
|
|
||||||
|
layer.animate(
|
||||||
|
from: previousStrokeEnd as NSNumber,
|
||||||
|
to: strokeEnd as NSNumber,
|
||||||
|
keyPath: "strokeEnd",
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
curve: curve,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setShapeLayerFillColor(layer: CAShapeLayer, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
if let current = layer.layerTintColor, current == color.cgColor {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
layer.fillColor = color.cgColor
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let previousColor: CGColor = layer.fillColor ?? UIColor.clear.cgColor
|
||||||
|
layer.fillColor = color.cgColor
|
||||||
|
|
||||||
|
layer.animate(
|
||||||
|
from: previousColor,
|
||||||
|
to: color.cgColor,
|
||||||
|
keyPath: "fillColor",
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
curve: curve,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||||
self.setBackgroundColor(layer: view.layer, color: color, completion: completion)
|
self.setBackgroundColor(layer: view.layer, color: color, completion: completion)
|
||||||
}
|
}
|
||||||
@ -819,4 +1064,18 @@ public struct Transition {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func animateContentsImage(layer: CALayer, from fromImage: CGImage, to toImage: CGImage, duration: Double, curve: Transition.Animation.Curve, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
layer.animate(
|
||||||
|
from: fromImage,
|
||||||
|
to: toImage,
|
||||||
|
keyPath: "contents",
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
curve: .easeInOut,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,20 +9,24 @@ public final class Button: Component {
|
|||||||
public let isEnabled: Bool
|
public let isEnabled: Bool
|
||||||
public let action: () -> Void
|
public let action: () -> Void
|
||||||
public let holdAction: (() -> Void)?
|
public let holdAction: (() -> Void)?
|
||||||
|
public let highlightedAction: ActionSlot<Bool>?
|
||||||
|
|
||||||
convenience public init(
|
convenience public init(
|
||||||
content: AnyComponent<Empty>,
|
content: AnyComponent<Empty>,
|
||||||
isEnabled: Bool = true,
|
isEnabled: Bool = true,
|
||||||
action: @escaping () -> Void
|
automaticHighlight: Bool = true,
|
||||||
|
action: @escaping () -> Void,
|
||||||
|
highlightedAction: ActionSlot<Bool>? = nil
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
content: content,
|
content: content,
|
||||||
minSize: nil,
|
minSize: nil,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
automaticHighlight: true,
|
automaticHighlight: automaticHighlight,
|
||||||
isEnabled: isEnabled,
|
isEnabled: isEnabled,
|
||||||
action: action,
|
action: action,
|
||||||
holdAction: nil
|
holdAction: nil,
|
||||||
|
highlightedAction: highlightedAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +37,8 @@ public final class Button: Component {
|
|||||||
automaticHighlight: Bool = true,
|
automaticHighlight: Bool = true,
|
||||||
isEnabled: Bool = true,
|
isEnabled: Bool = true,
|
||||||
action: @escaping () -> Void,
|
action: @escaping () -> Void,
|
||||||
holdAction: (() -> Void)?
|
holdAction: (() -> Void)?,
|
||||||
|
highlightedAction: ActionSlot<Bool>?
|
||||||
) {
|
) {
|
||||||
self.content = content
|
self.content = content
|
||||||
self.minSize = minSize
|
self.minSize = minSize
|
||||||
@ -42,6 +47,7 @@ public final class Button: Component {
|
|||||||
self.isEnabled = isEnabled
|
self.isEnabled = isEnabled
|
||||||
self.action = action
|
self.action = action
|
||||||
self.holdAction = holdAction
|
self.holdAction = holdAction
|
||||||
|
self.highlightedAction = highlightedAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public func minSize(_ minSize: CGSize?) -> Button {
|
public func minSize(_ minSize: CGSize?) -> Button {
|
||||||
@ -52,7 +58,8 @@ public final class Button: Component {
|
|||||||
automaticHighlight: self.automaticHighlight,
|
automaticHighlight: self.automaticHighlight,
|
||||||
isEnabled: self.isEnabled,
|
isEnabled: self.isEnabled,
|
||||||
action: self.action,
|
action: self.action,
|
||||||
holdAction: self.holdAction
|
holdAction: self.holdAction,
|
||||||
|
highlightedAction: self.highlightedAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +71,8 @@ public final class Button: Component {
|
|||||||
automaticHighlight: self.automaticHighlight,
|
automaticHighlight: self.automaticHighlight,
|
||||||
isEnabled: self.isEnabled,
|
isEnabled: self.isEnabled,
|
||||||
action: self.action,
|
action: self.action,
|
||||||
holdAction: holdAction
|
holdAction: holdAction,
|
||||||
|
highlightedAction: self.highlightedAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +84,8 @@ public final class Button: Component {
|
|||||||
automaticHighlight: self.automaticHighlight,
|
automaticHighlight: self.automaticHighlight,
|
||||||
isEnabled: self.isEnabled,
|
isEnabled: self.isEnabled,
|
||||||
action: self.action,
|
action: self.action,
|
||||||
holdAction: self.holdAction
|
holdAction: self.holdAction,
|
||||||
|
highlightedAction: self.highlightedAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,14 +111,21 @@ public final class Button: Component {
|
|||||||
public final class View: UIButton, ComponentTaggedView {
|
public final class View: UIButton, ComponentTaggedView {
|
||||||
private let contentView: ComponentHostView<Empty>
|
private let contentView: ComponentHostView<Empty>
|
||||||
|
|
||||||
|
public var content: UIView? {
|
||||||
|
return self.contentView.componentView
|
||||||
|
}
|
||||||
|
|
||||||
private var component: Button?
|
private var component: Button?
|
||||||
private var currentIsHighlighted: Bool = false {
|
private var currentIsHighlighted: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
guard let component = self.component, component.automaticHighlight else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.currentIsHighlighted != oldValue {
|
if self.currentIsHighlighted != oldValue {
|
||||||
self.updateAlpha(transition: .immediate)
|
if component.automaticHighlight {
|
||||||
|
self.updateAlpha(transition: .immediate)
|
||||||
|
}
|
||||||
|
component.highlightedAction?.invoke(self.currentIsHighlighted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,9 +153,12 @@ public final class Button: Component {
|
|||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.contentView = ComponentHostView<Empty>()
|
self.contentView = ComponentHostView<Empty>()
|
||||||
self.contentView.isUserInteractionEnabled = false
|
self.contentView.isUserInteractionEnabled = false
|
||||||
|
self.contentView.layer.allowsGroupOpacity = true
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.isExclusiveTouch = true
|
||||||
|
|
||||||
self.addSubview(self.contentView)
|
self.addSubview(self.contentView)
|
||||||
|
|
||||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
@ -4,13 +4,19 @@ import UIKit
|
|||||||
public final class Image: Component {
|
public final class Image: Component {
|
||||||
public let image: UIImage?
|
public let image: UIImage?
|
||||||
public let tintColor: UIColor?
|
public let tintColor: UIColor?
|
||||||
|
public let size: CGSize?
|
||||||
|
public let contentMode: UIImageView.ContentMode
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
image: UIImage?,
|
image: UIImage?,
|
||||||
tintColor: UIColor? = nil
|
tintColor: UIColor? = nil,
|
||||||
|
size: CGSize? = nil,
|
||||||
|
contentMode: UIImageView.ContentMode = .scaleToFill
|
||||||
) {
|
) {
|
||||||
self.image = image
|
self.image = image
|
||||||
self.tintColor = tintColor
|
self.tintColor = tintColor
|
||||||
|
self.size = size
|
||||||
|
self.contentMode = contentMode
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Image, rhs: Image) -> Bool {
|
public static func ==(lhs: Image, rhs: Image) -> Bool {
|
||||||
@ -20,6 +26,12 @@ public final class Image: Component {
|
|||||||
if lhs.tintColor != rhs.tintColor {
|
if lhs.tintColor != rhs.tintColor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.size != rhs.size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.contentMode != rhs.contentMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,8 +47,9 @@ public final class Image: Component {
|
|||||||
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
self.image = component.image
|
self.image = component.image
|
||||||
self.tintColor = component.tintColor
|
self.tintColor = component.tintColor
|
||||||
|
self.contentMode = component.contentMode
|
||||||
|
|
||||||
return availableSize
|
return component.size ?? availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ public final class RoundedRectangle: Component {
|
|||||||
}
|
}
|
||||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
|
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
} else if component.colors.count > 1{
|
} else if component.colors.count > 1 {
|
||||||
let imageSize = availableSize
|
let imageSize = availableSize
|
||||||
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
|
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
|
||||||
if let context = UIGraphicsGetCurrentContext() {
|
if let context = UIGraphicsGetCurrentContext() {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user