mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' into beta
This commit is contained in:
commit
ba1edc3cf6
640 changed files with 175089 additions and 5088 deletions
|
|
@ -4788,6 +4788,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"ContactList.Context.SendMessage" = "Send Message";
|
||||
"ContactList.Context.StartSecretChat" = "Start Secret Chat";
|
||||
"ContactList.Context.Call" = "Call";
|
||||
"ContactList.Context.VideoCall" = "Video Call";
|
||||
|
||||
"Theme.Context.Apply" = "Apply";
|
||||
|
||||
|
|
@ -5332,7 +5333,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"PeerInfo.ButtonMessage" = "Message";
|
||||
"PeerInfo.ButtonDiscuss" = "Discuss";
|
||||
"PeerInfo.ButtonCall" = "Call";
|
||||
"PeerInfo.ButtonVideoCall" = "Video Call";
|
||||
"PeerInfo.ButtonVideoCall" = "Video";
|
||||
"PeerInfo.ButtonMute" = "Mute";
|
||||
"PeerInfo.ButtonUnmute" = "Unmute";
|
||||
"PeerInfo.ButtonMore" = "More";
|
||||
|
|
@ -5710,3 +5711,8 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"Cache.MaximumCacheSize" = "Maximum Cache Size";
|
||||
"Cache.NoLimit" = "No Limit";
|
||||
"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
|
||||
|
||||
"Stats.MessageTitle" = "Message Statistics";
|
||||
"Stats.MessageOverview" = "Overview";
|
||||
"Stats.MessageInteractionsTitle" = "Interactions";
|
||||
"Stats.MessagePublicForwardsTitle" = "Public Shares";
|
||||
|
|
|
|||
|
|
@ -647,6 +647,7 @@ public protocol AccountContext: class {
|
|||
|
||||
var currentLimitsConfiguration: Atomic<LimitsConfiguration> { get }
|
||||
var currentContentSettings: Atomic<ContentSettings> { get }
|
||||
var currentAppConfiguration: Atomic<AppConfiguration> { get }
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
|
|
|||
|
|
@ -47,13 +47,19 @@ public func messageMediaFileCancelInteractiveFetch(context: AccountContext, mess
|
|||
context.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, userInitiated: true, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: true, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
let mediaReference = AnyMediaReference.message(message: messageReference, media: image)
|
||||
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: FetchCompleteRange, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
let ranges: IndexSet
|
||||
if let range = range {
|
||||
ranges = IndexSet(integersIn: range)
|
||||
} else {
|
||||
ranges = FetchCompleteRange
|
||||
}
|
||||
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
public func messageMediaImageCancelInteractiveFetch(context: AccountContext, messageId: MessageId, image: TelegramMediaImage, resource: MediaResource) {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public struct PresentationCallState: Equatable {
|
|||
case notAvailable
|
||||
case possible
|
||||
case outgoingRequested
|
||||
case incomingRequested
|
||||
case incomingRequested(sendsVideo: Bool)
|
||||
case active
|
||||
}
|
||||
|
||||
|
|
@ -81,17 +81,20 @@ public final class PresentationCallVideoView {
|
|||
|
||||
public let getOrientation: () -> Orientation
|
||||
public let setOnOrientationUpdated: (((Orientation) -> Void)?) -> Void
|
||||
public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void
|
||||
|
||||
public init(
|
||||
view: UIView,
|
||||
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void,
|
||||
getOrientation: @escaping () -> Orientation,
|
||||
setOnOrientationUpdated: @escaping (((Orientation) -> Void)?) -> Void
|
||||
setOnOrientationUpdated: @escaping (((Orientation) -> Void)?) -> Void,
|
||||
setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void
|
||||
) {
|
||||
self.view = view
|
||||
self.setOnFirstFrameReceived = setOnFirstFrameReceived
|
||||
self.getOrientation = getOrientation
|
||||
self.setOnOrientationUpdated = setOnOrientationUpdated
|
||||
self.setOnIsMirroredUpdated = setOnIsMirroredUpdated
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,5 +136,5 @@ public protocol PresentationCall: class {
|
|||
public protocol PresentationCallManager: class {
|
||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||
|
||||
func requestCall(account: Account, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import SyncCore
|
||||
|
||||
public struct VideoCallsConfiguration: Equatable {
|
||||
public enum VideoCallsSupport {
|
||||
case disabled
|
||||
case full
|
||||
case onlyVideo
|
||||
}
|
||||
|
||||
public var videoCallsSupport: VideoCallsSupport
|
||||
|
||||
public init(appConfiguration: AppConfiguration) {
|
||||
var videoCallsSupport: VideoCallsSupport = .full
|
||||
if let data = appConfiguration.data, let value = data["video_calls_support"] as? String {
|
||||
switch value {
|
||||
case "disabled":
|
||||
videoCallsSupport = .disabled
|
||||
case "full":
|
||||
videoCallsSupport = .full
|
||||
case "only_video":
|
||||
videoCallsSupport = .onlyVideo
|
||||
default:
|
||||
videoCallsSupport = .full
|
||||
}
|
||||
}
|
||||
self.videoCallsSupport = videoCallsSupport
|
||||
}
|
||||
}
|
||||
|
||||
public extension VideoCallsConfiguration {
|
||||
var areVideoCallsEnabled: Bool {
|
||||
switch self.videoCallsSupport {
|
||||
case .disabled:
|
||||
return false
|
||||
case .full, .onlyVideo:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import Display
|
|||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import YuvConversion
|
||||
|
||||
private let sharedQueue = Queue()
|
||||
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||
|
||||
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
||||
private var value: Bool = false
|
||||
|
|
@ -46,7 +48,7 @@ private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
|||
|
||||
public enum AnimatedStickerMode {
|
||||
case cached
|
||||
case direct
|
||||
case direct(cachePathPrefix: String?)
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackPosition {
|
||||
|
|
@ -267,11 +269,321 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
}
|
||||
}
|
||||
|
||||
private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int {
|
||||
return write(fd, data, count)
|
||||
}
|
||||
|
||||
private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
return read(fd, data, count)
|
||||
}
|
||||
|
||||
//TODO: separate ManagedFile into its own module
|
||||
private final class ManagedFileImpl {
|
||||
enum Mode {
|
||||
case read
|
||||
case readwrite
|
||||
case append
|
||||
}
|
||||
|
||||
private let queue: Queue?
|
||||
private let fd: Int32
|
||||
private let mode: Mode
|
||||
|
||||
init?(queue: Queue?, path: String, mode: Mode) {
|
||||
if let queue = queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
self.queue = queue
|
||||
self.mode = mode
|
||||
let fileMode: Int32
|
||||
let accessMode: UInt16
|
||||
switch mode {
|
||||
case .read:
|
||||
fileMode = O_RDONLY
|
||||
accessMode = S_IRUSR
|
||||
case .readwrite:
|
||||
fileMode = O_RDWR | O_CREAT
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
case .append:
|
||||
fileMode = O_WRONLY | O_CREAT | O_APPEND
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
}
|
||||
let fd = open(path, fileMode, accessMode)
|
||||
if fd >= 0 {
|
||||
self.fd = fd
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
close(self.fd)
|
||||
}
|
||||
|
||||
public func write(_ data: UnsafeRawPointer, count: Int) -> Int {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
return wrappedWrite(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
return wrappedRead(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func readData(count: Int) -> Data {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
var result = Data(count: count)
|
||||
result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Void in
|
||||
let readCount = self.read(bytes, count)
|
||||
assert(readCount == count)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func seek(position: Int64) {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
lseek(self.fd, position, SEEK_SET)
|
||||
}
|
||||
|
||||
public func truncate(count: Int64) {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
ftruncate(self.fd, count)
|
||||
}
|
||||
|
||||
public func getSize() -> Int? {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
var value = stat()
|
||||
if fstat(self.fd, &value) == 0 {
|
||||
return Int(value.st_size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func sync() {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
fsync(self.fd)
|
||||
}
|
||||
}
|
||||
|
||||
private func compressFrame(width: Int, height: Int, rgbData: Data) -> Data? {
|
||||
let bytesPerRow = rgbData.count / height
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||
|
||||
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||
var yuvaFrameData = malloc(yuvaLength)!
|
||||
defer {
|
||||
free(yuvaFrameData)
|
||||
}
|
||||
memset(yuvaFrameData, 0, yuvaLength)
|
||||
|
||||
var compressedFrameData = Data(count: yuvaLength)
|
||||
let compressedFrameDataLength = compressedFrameData.count
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
var rgbData = rgbData
|
||||
rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow))
|
||||
}
|
||||
}
|
||||
|
||||
var maybeResultSize: Int?
|
||||
|
||||
compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
|
||||
maybeResultSize = length
|
||||
}
|
||||
|
||||
guard let resultSize = maybeResultSize else {
|
||||
return nil
|
||||
}
|
||||
compressedFrameData.count = resultSize
|
||||
return compressedFrameData
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSourceCache {
|
||||
private let queue: Queue
|
||||
private let storeQueue: Queue
|
||||
private let file: ManagedFileImpl
|
||||
private let frameCount: Int
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
|
||||
private var isStoringFrames = Set<Int>()
|
||||
|
||||
private var scratchBuffer: Data
|
||||
private var decodeBuffer: Data
|
||||
|
||||
init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int) {
|
||||
self.queue = queue
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.frameCount = frameCount
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
let path = "\(pathPrefix)_\(width):\(height).stickerframecache"
|
||||
var file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||
self.decodeBuffer = Data(count: yuvaLength)
|
||||
|
||||
self.initializeFrameTable()
|
||||
}
|
||||
|
||||
private func initializeFrameTable() {
|
||||
if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 {
|
||||
} else {
|
||||
self.file.truncate(count: 0)
|
||||
for _ in 0 ..< self.frameCount {
|
||||
var zero: Int32 = 0
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readFrameRange(index: Int) -> Range<Int>? {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
return nil
|
||||
}
|
||||
if self.file.read(&length, 4) != 4 {
|
||||
return nil
|
||||
}
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
if length < 0 || offset < 0 {
|
||||
return nil
|
||||
}
|
||||
return (Int(offset) ..< Int(offset + length))
|
||||
}
|
||||
|
||||
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return
|
||||
}
|
||||
if self.isStoringFrames.contains(index) {
|
||||
return
|
||||
}
|
||||
self.isStoringFrames.insert(index)
|
||||
|
||||
let width = self.width
|
||||
let height = self.height
|
||||
|
||||
let queue = self.queue
|
||||
self.storeQueue.async { [weak self] in
|
||||
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData)
|
||||
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let currentSize = strongSelf.file.getSize() else {
|
||||
return
|
||||
}
|
||||
guard let compressedData = compressedData else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset = Int32(currentSize)
|
||||
var length = Int32(compressedData.count)
|
||||
let _ = strongSelf.file.write(&offset, count: 4)
|
||||
let _ = strongSelf.file.write(&length, count: 4)
|
||||
strongSelf.file.seek(position: Int64(currentSize))
|
||||
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readUncompressedYuvFrame(index: Int) -> Data? {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return nil
|
||||
}
|
||||
guard let range = self.readFrameRange(index: index) else {
|
||||
return nil
|
||||
}
|
||||
self.file.seek(position: Int64(range.lowerBound))
|
||||
let length = range.upperBound - range.lowerBound
|
||||
let compressedData = self.file.readData(count: length)
|
||||
if compressedData.count != length {
|
||||
return nil
|
||||
}
|
||||
|
||||
var frameData: Data?
|
||||
|
||||
let decodeBufferLength = self.decodeBuffer.count
|
||||
|
||||
compressedData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameData
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
|
|
@ -282,7 +594,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int) {
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
|
|
@ -294,8 +606,13 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||
return nil
|
||||
}
|
||||
self.animation = animation
|
||||
self.frameCount = Int(animation.frameCount)
|
||||
let frameCount = Int(animation.frameCount)
|
||||
self.frameCount = frameCount
|
||||
self.frameRate = Int(animation.frameRate)
|
||||
|
||||
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -306,12 +623,19 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
} else {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||
}
|
||||
if let cache = self.cache {
|
||||
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -409,7 +733,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
||||
|
||||
private var directData: (Data, String, Int, Int)?
|
||||
private var directData: (Data, String, Int, Int, String?)?
|
||||
private var cachedData: (Data, Bool)?
|
||||
|
||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||
|
|
@ -479,13 +803,13 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
}
|
||||
self.playbackMode = playbackMode
|
||||
switch mode {
|
||||
case .direct:
|
||||
case let .direct(cachePathPrefix):
|
||||
let f: (String) -> Void = { [weak self] path in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
strongSelf.directData = (directData, path, width, height)
|
||||
strongSelf.directData = (directData, path, width, height, cachePathPrefix)
|
||||
}
|
||||
if case let .still(position) = playbackMode {
|
||||
strongSelf.seekTo(position)
|
||||
|
|
@ -568,7 +892,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
if maybeFrameSource == nil {
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
|
|
@ -640,7 +964,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
|
|
@ -730,7 +1054,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
} else {
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
if case .end = position {
|
||||
maybeFrameSource?.skipToEnd()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ import YuvConversion
|
|||
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) {
|
||||
queue.async { [weak self] in
|
||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
switch type {
|
||||
case .argb:
|
||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
case .yuva:
|
||||
break
|
||||
}
|
||||
|
||||
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
|
||||
switch type {
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ public final class CallListController: ViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(context: strongSelf.context, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult {
|
||||
if case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
|
|
@ -291,7 +291,7 @@ public final class CallListController: ViewController {
|
|||
if let strongSelf = self, let peer = peer, let current = current {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(context: strongSelf.context, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||||
began?()
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||
animationName = "ChatListEmpty"
|
||||
}
|
||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
|
|||
animationName = "ChatListNewFolder"
|
||||
}
|
||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
strongSelf.animationNode.visibility = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,15 +113,17 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
}
|
||||
var canVideoCall = false
|
||||
if canCall {
|
||||
if context.sharedContext.immediateExperimentalUISettings.videoCalls {
|
||||
canVideoCall = true
|
||||
if let cachedUserData = transaction.getPeerCachedData(peerId: peerId) as? CachedUserData {
|
||||
if cachedUserData.videoCallsAvailable {
|
||||
canVideoCall = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if canCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_Call, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -133,7 +135,7 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
|
|
@ -144,10 +146,9 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
})))
|
||||
}
|
||||
if canVideoCall {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Video Call", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_VideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -159,7 +160,7 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ static_library(
|
|||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ swift_library(
|
|||
name = "ImageCompression",
|
||||
module_name = "ImageCompression",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import MozjpegBinding
|
||||
|
||||
public func extractImageExtraScans(_ data: Data) -> [Int] {
|
||||
return extractJPEGDataScans(data).map { item in
|
||||
return item.intValue
|
||||
}
|
||||
}
|
||||
|
||||
public func compressImageToJPEG(_ image: UIImage, quality: Float) -> Data? {
|
||||
if let result = compressJPEGData(image) {
|
||||
return result
|
||||
}
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, "public.jpeg" as CFString, 1, nil) else {
|
||||
return nil
|
||||
|
|
@ -46,3 +57,7 @@ public func compressImage(_ image: UIImage, quality: Float) -> Data? {
|
|||
|
||||
return data as Data
|
||||
}
|
||||
|
||||
public func compressImageMiniThumbnail(_ image: UIImage) -> Data? {
|
||||
return compressMiniThumbnail(image)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
representations.append(contentsOf: file.previewRepresentations)
|
||||
if let dimensions = file.dimensions {
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: []))
|
||||
}
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
thumbnailItem = .animated(item.file.resource)
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource)
|
||||
} else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource))
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: []))
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
{
|
||||
localizationPlaceholderText = TGLocalized(@"MediaPicker.AddCaption");
|
||||
NSString *placeholderText = TGLocalized(@"MediaPicker.AddCaption");
|
||||
UIFont *placeholderFont = TGSystemFontOfSize(16);
|
||||
UIFont *placeholderFont = TGSystemFontOfSize(17);
|
||||
CGSize placeholderSize = [placeholderText sizeWithFont:placeholderFont];
|
||||
placeholderSize.width += 2.0f;
|
||||
placeholderSize.height += 2.0f;
|
||||
|
|
@ -121,7 +121,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_placeholderLabel = [[UILabel alloc] init];
|
||||
_placeholderLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
|
||||
_placeholderLabel.backgroundColor = [UIColor clearColor];
|
||||
_placeholderLabel.font = TGSystemFontOfSize(16);
|
||||
_placeholderLabel.font = TGSystemFontOfSize(17);
|
||||
_placeholderLabel.textColor = UIColorRGB(0x7f7f7f);
|
||||
_placeholderLabel.text = TGLocalized(@"MediaPicker.AddCaption");
|
||||
_placeholderLabel.userInteractionEnabled = true;
|
||||
|
|
@ -130,7 +130,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
|
||||
_inputFieldOnelineLabel = [[UILabel alloc] init];
|
||||
_inputFieldOnelineLabel.backgroundColor = [UIColor clearColor];
|
||||
_inputFieldOnelineLabel.font = TGSystemFontOfSize(16);
|
||||
_inputFieldOnelineLabel.font = TGSystemFontOfSize(17);
|
||||
_inputFieldOnelineLabel.hidden = true;
|
||||
_inputFieldOnelineLabel.numberOfLines = 1;
|
||||
_inputFieldOnelineLabel.textColor = [UIColor whiteColor];
|
||||
|
|
@ -169,7 +169,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_inputField.textColor = [UIColor whiteColor];
|
||||
_inputField.disableFormatting = !_allowEntities;
|
||||
_inputField.placeholderView = _placeholderLabel;
|
||||
_inputField.font = TGSystemFontOfSize(16);
|
||||
_inputField.font = TGSystemFontOfSize(17);
|
||||
_inputField.accentColor = UIColorRGB(0x78b1f9);
|
||||
_inputField.clipsToBounds = true;
|
||||
_inputField.backgroundColor = nil;
|
||||
|
|
@ -188,7 +188,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
|
||||
_inputField.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(-inputFieldInternalEdgeInsets.top, 0, 5 - TGRetinaPixel, 0);
|
||||
|
||||
[_inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:16.0f] keepFormatting:true animated:false];
|
||||
[_inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:17.0f] keepFormatting:true animated:false];
|
||||
|
||||
[_inputFieldClippingContainer addSubview:_inputField];
|
||||
}
|
||||
|
|
@ -439,7 +439,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_fieldBackground.alpha = _placeholderLabel.hidden ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
[self.inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:16.0f] keepFormatting:true animated:false];
|
||||
[self.inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:17.0f] keepFormatting:true animated:false];
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)attributedStringForText:(NSString *)text entities:(NSArray *)entities fontSize:(CGFloat)fontSize {
|
||||
|
|
@ -777,14 +777,14 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
if (text == nil)
|
||||
return nil;
|
||||
|
||||
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:[TGMediaPickerCaptionInputPanel attributedStringForText:text entities:entities fontSize:16.0f]];
|
||||
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:[TGMediaPickerCaptionInputPanel attributedStringForText:text entities:entities fontSize:17.0f]];
|
||||
|
||||
for (NSUInteger i = 0; i < string.length; i++)
|
||||
{
|
||||
unichar c = [text characterAtIndex:i];
|
||||
if (c == '\t' || c == '\n')
|
||||
{
|
||||
[string insertAttributedString:[[NSAttributedString alloc] initWithString:tokenString attributes:@{NSFontAttributeName:TGSystemFontOfSize(16.0f)}] atIndex:i];
|
||||
[string insertAttributedString:[[NSAttributedString alloc] initWithString:tokenString attributes:@{NSFontAttributeName:TGSystemFontOfSize(17.0f)}] atIndex:i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,13 +167,14 @@
|
|||
if (recipientName.length > 0)
|
||||
{
|
||||
_arrowView = [[UIImageView alloc] initWithImage: TGComponentsImageNamed(@"PhotoPickerArrow")];
|
||||
_arrowView.alpha = 0.45f;
|
||||
_arrowView.alpha = 0.6f;
|
||||
[_wrapperView addSubview:_arrowView];
|
||||
|
||||
_recipientLabel = [[UILabel alloc] init];
|
||||
_recipientLabel.alpha = 0.6;
|
||||
_recipientLabel.backgroundColor = [UIColor clearColor];
|
||||
_recipientLabel.font = TGBoldSystemFontOfSize(13.0f);
|
||||
_recipientLabel.textColor = UIColorRGBA(0xffffff, 0.45f);
|
||||
_recipientLabel.textColor = UIColorRGB(0xffffff);
|
||||
_recipientLabel.text = recipientName;
|
||||
_recipientLabel.userInteractionEnabled = false;
|
||||
[_recipientLabel sizeToFit];
|
||||
|
|
@ -510,13 +511,7 @@
|
|||
UIEdgeInsets screenEdges = [self screenEdges];
|
||||
|
||||
__weak TGMediaPickerGalleryInterfaceView *weakSelf = self;
|
||||
|
||||
if ([itemView.headerView isKindOfClass:[TGMediaPickerScrubberHeaderView class]])
|
||||
{
|
||||
TGMediaPickerScrubberHeaderView *headerView = (TGMediaPickerScrubberHeaderView *)itemView.headerView;
|
||||
[headerView.scrubberView setRecipientName:_recipientLabel.text];
|
||||
}
|
||||
|
||||
|
||||
[self _layoutRecipientLabelForOrientation:[self interfaceOrientation] screenEdges:screenEdges hasHeaderView:(itemView.headerView != nil)];
|
||||
|
||||
if (_selectionContext != nil)
|
||||
|
|
@ -1039,8 +1034,8 @@
|
|||
{
|
||||
_checkButton.alpha = alpha;
|
||||
_muteButton.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
} completion:^(BOOL finished)
|
||||
{
|
||||
if (finished)
|
||||
|
|
@ -1070,8 +1065,8 @@
|
|||
_muteButton.alpha = alpha;
|
||||
_muteButton.userInteractionEnabled = !hidden;
|
||||
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
}
|
||||
|
||||
if (hidden)
|
||||
|
|
@ -1095,7 +1090,7 @@
|
|||
{
|
||||
_checkButton.alpha = alpha;
|
||||
_muteButton.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_arrowView.alpha = alpha * 0.6;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_portraitToolbarView.alpha = alpha;
|
||||
_landscapeToolbarView.alpha = alpha;
|
||||
|
|
@ -1132,7 +1127,7 @@
|
|||
_muteButton.alpha = alpha;
|
||||
_muteButton.userInteractionEnabled = !hidden;
|
||||
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_arrowView.alpha = alpha * 0.6;
|
||||
_recipientLabel.alpha = alpha;
|
||||
|
||||
_portraitToolbarView.alpha = alpha;
|
||||
|
|
@ -1391,6 +1386,8 @@
|
|||
screenEdges.left += _safeAreaInset.left;
|
||||
screenEdges.right -= _safeAreaInset.right;
|
||||
|
||||
CGFloat panelInset = 0.0f;
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
|
|
@ -1402,13 +1399,10 @@
|
|||
break;
|
||||
|
||||
default:
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.top + 6, _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasHeaderView)
|
||||
frame.origin.y += 64;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
|
@ -1462,9 +1456,6 @@
|
|||
break;
|
||||
}
|
||||
|
||||
if (hasHeaderView)
|
||||
frame.origin.y += 64;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
|
@ -1526,9 +1517,6 @@
|
|||
|
||||
_arrowView.frame = frame;
|
||||
_recipientLabel.frame = CGRectMake(CGRectGetMaxX(_arrowView.frame) + 6.0f, _arrowView.frame.origin.y - 2.0f, recipientWidth, _recipientLabel.frame.size.height);
|
||||
|
||||
_arrowView.hidden = hasHeaderView;
|
||||
_recipientLabel.hidden = hasHeaderView;
|
||||
}
|
||||
|
||||
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)__unused duration
|
||||
|
|
@ -1670,15 +1658,14 @@
|
|||
{
|
||||
[UIView performWithoutAnimation:^
|
||||
{
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom, 64, 38);
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 64.0 : 0.0), 64, 38);
|
||||
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom, self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 64.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
}];
|
||||
|
||||
_landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height);
|
||||
|
||||
CGFloat topInset = _safeAreaInset.top > FLT_EPSILON ? _safeAreaInset.top - 14.0 : 0.0f;
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.top + topInset, self.frame.size.width, 64);
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 64.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
bool _scrubbingPanelPresented;
|
||||
bool _scrubbingPanelLocked;
|
||||
bool _shouldResetScrubber;
|
||||
NSArray *_cachedThumbnails;
|
||||
UIImage *_immediateThumbnail;
|
||||
|
||||
UILabel *_fileInfoLabel;
|
||||
|
||||
|
|
@ -215,13 +217,12 @@
|
|||
_headerView = headerView;
|
||||
_headerView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
|
||||
_scrubberPanelView = [[UIView alloc] initWithFrame:CGRectMake(0, -64, _headerView.frame.size.width, 64)];
|
||||
_scrubberPanelView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _headerView.frame.size.width, 64)];
|
||||
_scrubberPanelView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
_scrubberPanelView.hidden = true;
|
||||
headerView.panelView = _scrubberPanelView;
|
||||
[_headerView addSubview:_scrubberPanelView];
|
||||
|
||||
UIView *scrubberBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, -100.0f, _headerView.frame.size.width, 164.0f)];
|
||||
UIView *scrubberBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _headerView.frame.size.width, 64.0f)];
|
||||
scrubberBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
scrubberBackgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor];
|
||||
[_scrubberPanelView addSubview:scrubberBackgroundView];
|
||||
|
|
@ -301,7 +302,6 @@
|
|||
|
||||
_appeared = false;
|
||||
[self setScrubbingPanelApperanceLocked:false];
|
||||
[self setScrubbingPanelHidden:true animated:false];
|
||||
|
||||
[_positionTimer invalidate];
|
||||
_positionTimer = nil;
|
||||
|
|
@ -386,9 +386,14 @@
|
|||
- (void)setItem:(TGMediaPickerGalleryVideoItem *)item synchronously:(bool)synchronously
|
||||
{
|
||||
bool itemChanged = ![item isEqual:self.item];
|
||||
bool itemIdChanged = item.uniqueId != self.item.uniqueId;
|
||||
|
||||
[super setItem:item synchronously:synchronously];
|
||||
|
||||
if (itemIdChanged) {
|
||||
_immediateThumbnail = item.immediateThumbnailImage;
|
||||
}
|
||||
|
||||
if (itemChanged) {
|
||||
[self _playerCleanup];
|
||||
|
||||
|
|
@ -618,17 +623,15 @@
|
|||
|
||||
void (^changeBlock)(void) = ^
|
||||
{
|
||||
_scrubberPanelView.frame = CGRectMake(0.0f, -64.0f - _safeAreaInset.top, _scrubberPanelView.frame.size.width, _scrubberPanelView.frame.size.height);
|
||||
_scrubberPanelView.alpha = 0.0f;
|
||||
};
|
||||
void (^completionBlock)(BOOL) = ^(BOOL finished)
|
||||
{
|
||||
if (finished)
|
||||
_scrubberPanelView.hidden = true;
|
||||
};
|
||||
|
||||
if (animated)
|
||||
{
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:changeBlock completion:completionBlock];
|
||||
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:changeBlock completion:completionBlock];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -642,18 +645,17 @@
|
|||
return;
|
||||
|
||||
_scrubbingPanelPresented = true;
|
||||
|
||||
_scrubberPanelView.hidden = false;
|
||||
|
||||
[_scrubberPanelView layoutSubviews];
|
||||
[_scrubberView layoutSubviews];
|
||||
|
||||
void (^changeBlock)(void) = ^
|
||||
{
|
||||
_scrubberPanelView.frame = CGRectMake(0.0f, 0.0f, _scrubberPanelView.frame.size.width, _scrubberPanelView.frame.size.height);
|
||||
_scrubberPanelView.alpha = 1.0f;
|
||||
};
|
||||
|
||||
if (animated)
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:changeBlock completion:nil];
|
||||
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:changeBlock completion:nil];
|
||||
else
|
||||
changeBlock();
|
||||
}
|
||||
|
|
@ -708,7 +710,6 @@
|
|||
{
|
||||
[_scrubberView resetThumbnails];
|
||||
|
||||
[self setScrubbingPanelHidden:true animated:false];
|
||||
[_scrubberPanelView setNeedsLayout];
|
||||
[_scrubberPanelView layoutIfNeeded];
|
||||
|
||||
|
|
@ -722,11 +723,14 @@
|
|||
if (_containerView == nil)
|
||||
return;
|
||||
|
||||
_containerView.frame = self.bounds;
|
||||
if (self.bounds.size.width > self.bounds.size.height)
|
||||
_containerView.frame = self.bounds;
|
||||
else
|
||||
_containerView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height - 44.0);
|
||||
|
||||
[self _layoutPlayerView];
|
||||
|
||||
_videoContentView.frame = (CGRect){CGPointZero, frame.size};
|
||||
_videoContentView.frame = (CGRect){CGPointZero, _containerView.frame.size};
|
||||
|
||||
if (_tooltipContainerView != nil && frame.size.width > frame.size.height)
|
||||
{
|
||||
|
|
@ -753,8 +757,6 @@
|
|||
mirrored = adjustments.cropMirrored;
|
||||
}
|
||||
|
||||
// _scrubberView.maximumLength = adjustments.sendAsGif ? TGVideoEditMaximumGifDuration : 0.0;
|
||||
|
||||
[self _layoutPlayerViewWithCropRect:cropRect videoFrameSize:videoFrameSize orientation:orientation mirrored:mirrored];
|
||||
}
|
||||
|
||||
|
|
@ -1440,40 +1442,8 @@
|
|||
trimEndValue = adjustments.trimEndValue;
|
||||
}
|
||||
}
|
||||
// NSTimeInterval trimDuration = trimEndValue - trimStartValue;
|
||||
|
||||
bool sendAsGif = !adjustments.sendAsGif;
|
||||
// if (sendAsGif && _scrubberView.allowsTrimming)
|
||||
// {
|
||||
// if (trimDuration > TGVideoEditMaximumGifDuration)
|
||||
// {
|
||||
// trimEndValue = trimStartValue + TGVideoEditMaximumGifDuration;
|
||||
//
|
||||
// if (_scrubberView.value > trimEndValue)
|
||||
// {
|
||||
// [self stop];
|
||||
// [_scrubberView setValue:_scrubberView.trimStartValue resetPosition:true];
|
||||
// [self _seekToPosition:_scrubberView.value manual:true];
|
||||
// }
|
||||
//
|
||||
// _scrubberView.trimStartValue = trimStartValue;
|
||||
// _scrubberView.trimEndValue = trimEndValue;
|
||||
// [_scrubberView setTrimApplied:true];
|
||||
// [self updatePlayerRange:trimEndValue];
|
||||
// }
|
||||
// }
|
||||
// else if (_shouldResetScrubber)
|
||||
// {
|
||||
// trimStartValue = 0.0;
|
||||
// trimEndValue = _videoDuration;
|
||||
//
|
||||
// _scrubberView.trimStartValue = trimStartValue;
|
||||
// _scrubberView.trimEndValue = trimEndValue;
|
||||
//
|
||||
// [_scrubberView setTrimApplied:false];
|
||||
// [self updatePlayerRange:trimEndValue];
|
||||
// }
|
||||
|
||||
TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:adjustments.cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:sendAsGif preset:adjustments.preset];
|
||||
[self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem];
|
||||
|
||||
|
|
@ -1596,6 +1566,23 @@
|
|||
return timestamps;
|
||||
}
|
||||
|
||||
- (SSignal *)_placeholderThumbnails:(NSArray *)timestamps {
|
||||
NSMutableArray *thumbnails = [[NSMutableArray alloc] init];
|
||||
|
||||
UIImage *image = _immediateThumbnail;
|
||||
if (image == nil)
|
||||
return [SSignal complete];
|
||||
|
||||
UIImage *blurredImage = TGBlurredRectangularImage(image, true, image.size, image.size, NULL, nil);
|
||||
for (__unused NSNumber *value in timestamps) {
|
||||
if (thumbnails.count == 0)
|
||||
[thumbnails addObject:image];
|
||||
else
|
||||
[thumbnails addObject:blurredImage];
|
||||
}
|
||||
return [SSignal single:thumbnails];
|
||||
}
|
||||
|
||||
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)__unused videoScrubber requestThumbnailImagesForTimestamps:(NSArray *)timestamps size:(CGSize)size isSummaryThumbnails:(bool)isSummaryThumbnails
|
||||
{
|
||||
if (timestamps.count == 0)
|
||||
|
|
@ -1605,17 +1592,42 @@
|
|||
TGMediaEditingContext *editingContext = self.item.editingContext;
|
||||
id<TGMediaEditableItem> editableItem = self.editableMediaItem;
|
||||
|
||||
SSignal *thumbnailsSignal = nil;
|
||||
if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto])
|
||||
thumbnailsSignal = [TGMediaAssetImageSignals videoThumbnailsForAsset:self.item.asset size:size timestamps:timestamps];
|
||||
else if (avAsset != nil)
|
||||
thumbnailsSignal = [avAsset mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
return [TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps];
|
||||
}];
|
||||
// SSignal *thumbnailsSignal = nil;
|
||||
// if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto])
|
||||
// thumbnailsSignal = [TGMediaAssetImageSignals videoThumbnailsForAsset:self.item.asset size:size timestamps:timestamps];
|
||||
// else if (avAsset != nil)
|
||||
// thumbnailsSignal = [avAsset mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
// return [TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps];
|
||||
// }];
|
||||
|
||||
__strong TGMediaPickerGalleryVideoItemView *weakSelf = self;
|
||||
SSignal *thumbnailsSignal = nil;
|
||||
if (_cachedThumbnails != nil) {
|
||||
thumbnailsSignal = [SSignal single:_cachedThumbnails];
|
||||
} else if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto]) {
|
||||
thumbnailsSignal = [[self _placeholderThumbnails:timestamps] then:[[TGMediaAssetImageSignals videoThumbnailsForAsset:(TGMediaAsset *)self.item.asset size:size timestamps:timestamps] onNext:^(NSArray *images) {
|
||||
__strong TGMediaPickerGalleryVideoItemView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf->_cachedThumbnails == nil)
|
||||
strongSelf->_cachedThumbnails = images;
|
||||
}]];
|
||||
} else if ([self.item.asset isKindOfClass:[TGCameraCapturedVideo class]]) {
|
||||
thumbnailsSignal = [[((TGCameraCapturedVideo *)self.item.asset).avAsset takeLast] mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
return [[self _placeholderThumbnails:timestamps] then:[[TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps] onNext:^(NSArray *images) {
|
||||
__strong TGMediaPickerGalleryVideoItemView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf->_cachedThumbnails == nil)
|
||||
strongSelf->_cachedThumbnails = images;
|
||||
}]];
|
||||
}];
|
||||
}
|
||||
|
||||
_requestingThumbnails = true;
|
||||
|
||||
__weak TGMediaPickerGalleryVideoItemView *weakSelf = self;
|
||||
[_thumbnailsDisposable setDisposable:[[[thumbnailsSignal map:^NSArray *(NSArray *images) {
|
||||
id<TGMediaEditAdjustments> adjustments = [editingContext adjustmentsForItem:editableItem];
|
||||
if (adjustments.toolsApplied) {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@
|
|||
|
||||
- (void)setThumbnailImage:(UIImage *)image forTimestamp:(NSTimeInterval)timestamp index:(NSInteger)index isSummaryThubmnail:(bool)isSummaryThumbnail;
|
||||
|
||||
- (void)setRecipientName:(NSString *)recipientName;
|
||||
|
||||
- (CGPoint)scrubberPositionForPosition:(NSTimeInterval)position;
|
||||
|
||||
- (void)_updateScrubberAnimationsAndResetCurrentPosition:(bool)resetCurrentPosition;
|
||||
|
|
|
|||
|
|
@ -81,9 +81,6 @@ typedef enum
|
|||
NSInteger _zoomedPivotTimestampIndex;
|
||||
NSArray *_zoomedTimestamps;
|
||||
NSMutableArray *_zoomedThumbnailViews;
|
||||
|
||||
UIImageView *_arrowView;
|
||||
UILabel *_recipientLabel;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -417,33 +414,10 @@ typedef enum
|
|||
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
_tapGestureRecognizer.enabled = false;
|
||||
[_trimView addGestureRecognizer:_tapGestureRecognizer];
|
||||
|
||||
_arrowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PhotoPickerArrow")];
|
||||
_arrowView.alpha = 0.45f;
|
||||
_arrowView.hidden = true;
|
||||
[self addSubview:_arrowView];
|
||||
|
||||
_recipientLabel = [[UILabel alloc] init];
|
||||
_recipientLabel.backgroundColor = [UIColor clearColor];
|
||||
_recipientLabel.font = TGBoldSystemFontOfSize(13.0f);
|
||||
_recipientLabel.textColor = UIColorRGBA(0xffffff, 0.45f);
|
||||
_recipientLabel.hidden = true;
|
||||
[self addSubview:_recipientLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setRecipientName:(NSString *)recipientName
|
||||
{
|
||||
_recipientLabel.text = recipientName;
|
||||
_recipientLabel.hidden = recipientName.length == 0;
|
||||
_arrowView.hidden = _recipientLabel.hidden;
|
||||
|
||||
[_recipientLabel sizeToFit];
|
||||
|
||||
[self _layoutRecipientLabel];
|
||||
}
|
||||
|
||||
- (void)setHasDotPicker:(bool)hasDotPicker {
|
||||
_hasDotPicker = hasDotPicker;
|
||||
_tapGestureRecognizer.enabled = hasDotPicker;
|
||||
|
|
@ -1052,11 +1026,9 @@ typedef enum
|
|||
|
||||
- (void)_updateTimeLabels
|
||||
{
|
||||
_currentTimeLabel.text = @"";
|
||||
_currentTimeLabel.text = self.disableTimeDisplay ? @"" : [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.value];
|
||||
|
||||
NSString *text = [NSString stringWithFormat:@"%@ / %@", [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.value], [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.duration]];
|
||||
|
||||
_inverseTimeLabel.text = self.disableTimeDisplay ? @"" : text;
|
||||
_inverseTimeLabel.text = self.disableTimeDisplay ? @"" : [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.duration];
|
||||
}
|
||||
|
||||
#pragma mark - Scrubber Handle
|
||||
|
|
@ -1466,18 +1438,6 @@ typedef enum
|
|||
}
|
||||
}
|
||||
|
||||
- (void)_layoutRecipientLabel
|
||||
{
|
||||
if (self.frame.size.width < FLT_EPSILON)
|
||||
return;
|
||||
|
||||
CGFloat screenWidth = MAX(self.frame.size.width, self.frame.size.height);
|
||||
CGFloat recipientWidth = MIN(_recipientLabel.frame.size.width, screenWidth - 100.0f);
|
||||
|
||||
_arrowView.frame = CGRectMake(14.0f, 6.0f, _arrowView.frame.size.width, _arrowView.frame.size.height);
|
||||
_recipientLabel.frame = CGRectMake(CGRectGetMaxX(_arrowView.frame) + 6.0f, _arrowView.frame.origin.y - 2.0f, recipientWidth, _recipientLabel.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
if (isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height))
|
||||
|
|
@ -1501,8 +1461,6 @@ typedef enum
|
|||
_zoomedThumbnailWrapperView.frame = _summaryThumbnailWrapperView.frame;
|
||||
|
||||
[self _updateScrubberAnimationsAndResetCurrentPosition:true];
|
||||
|
||||
[self _layoutRecipientLabel];
|
||||
}
|
||||
|
||||
+ (NSString *)_stringFromTotalSeconds:(NSInteger)totalSeconds
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
|
|||
{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(38.0f, 38.0f), false, 0.0f);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.7f).CGColor);
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.3f).CGColor);
|
||||
|
||||
CGContextFillEllipseInRect(context, CGRectMake(3.5f, 1.0f, 31.0f, 31.0f));
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
CGRect rect = CGRectMake(0, 0, 39.0f, 39.0f);
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.6f).CGColor);
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.3f).CGColor);
|
||||
CGContextFillEllipseInRect(context, CGRectInset(rect, 3, 3));
|
||||
|
||||
muteBackground = UIGraphicsGetImageFromCurrentImageContext();
|
||||
|
|
|
|||
|
|
@ -1216,7 +1216,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
if (![editorController isKindOfClass:[TGPhotoEditorController class]])
|
||||
return;
|
||||
|
||||
if (hasStickers) {
|
||||
if (!hasStickers) {
|
||||
[editorController setMinimalVideoDuration:duration];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc
|
|||
if let resourcePath = resourcePath, let image = UIImage(contentsOfFile: resourcePath) {
|
||||
dimensions = image.size
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimensions), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimensions), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc
|
|||
if let image = UIImage(contentsOfFile: fullSizePath) {
|
||||
let resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
|
||||
copyLocalFiles.append((resource, fullSizePath))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc
|
|||
} else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path {
|
||||
copyLocalFiles.append((resource, path))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -488,7 +488,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc
|
|||
resource = updatedResource
|
||||
copyLocalFiles.append((resource, pathFromLegacyImageUrl(basePath: basePath, url: imageUrl)))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ func loadLegacyUser(database: SqliteInterface, id: Int32) -> (TelegramUser, Tele
|
|||
let photoBig = cursor.getString(at: 6)
|
||||
var photo: [TelegramMediaImageRepresentation] = []
|
||||
if let resource = resourceFromLegacyImageUrl(photoSmall) {
|
||||
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: resource))
|
||||
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
if let resource = resourceFromLegacyImageUrl(photoBig) {
|
||||
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 600, height: 600), resource: resource))
|
||||
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 600, height: 600), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
|
||||
let user = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: cursor.getInt32(at: 0)), accessHash: accessHash == 0 ? nil : .personal(accessHash), firstName: firstName.isEmpty ? nil : firstName, lastName: lastName.isEmpty ? nil : lastName, username: username.isEmpty ? nil : username, phone: phone.isEmpty ? nil : phone, photo: photo, botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ public func legacyEnqueueGifMessage(account: Account, data: Data) -> Signal<Enqu
|
|||
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData)
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
|
||||
var randomId: Int64 = 0
|
||||
|
|
@ -312,7 +312,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
|
|||
let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)!
|
||||
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
switch data {
|
||||
|
|
@ -324,26 +324,9 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
|
|||
if let scaledImage = TGScaleImageToPixelSize(image, scaledSize) {
|
||||
if let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.6) {
|
||||
let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath))
|
||||
#if DEBUG
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
if false, let heicData = compressImage(scaledImage, quality: 0.65) {
|
||||
print("scaledImageData \(scaledImageData.count), heicData \(heicData.count)")
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
let _ = try? heicData.write(to: URL(fileURLWithPath: tempFilePath + ".heic"))
|
||||
let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath + ".heic", randomId: randomId)
|
||||
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/heic", size: nil, attributes: [.FileName(fileName: "image.heic")])
|
||||
var attributes: [MessageAttribute] = []
|
||||
if let timer = item.timer, timer > 0 && timer <= 60 {
|
||||
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
|
||||
}
|
||||
messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: []))
|
||||
|
||||
var imageFlags: TelegramMediaImageFlags = []
|
||||
|
||||
|
|
@ -375,7 +358,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
|
|||
let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight))
|
||||
let scaledSize = size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0))
|
||||
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: arc4random64())
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: []))
|
||||
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
|
@ -442,7 +425,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
|
|||
let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)!
|
||||
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData)
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView {
|
|||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource)
|
||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct)
|
||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
|
||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource {
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if let legacyThumbnailUri = args["legacyThumbnailUri"] as? String, let data = Data(base64Encoded: legacyThumbnailUri, options: []) {
|
||||
if let resource = PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? TelegramMediaResource {
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 140, height: 140), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 140, height: 140), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,19 +52,7 @@ public func fetchPhotoLibraryResource(localIdentifier: String) -> Signal<MediaRe
|
|||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
if let scaledImage = scaledImage, let data = scaledImage.jpegData(compressionQuality: 0.8) {
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
#if DEBUG
|
||||
if false, let heicData = compressImage(scaledImage, quality: 0.8) {
|
||||
//compressTinyThumbnail(scaledImage)
|
||||
print("data \(data.count), heicData \(heicData.count)")
|
||||
subscriber.putNext(.dataPart(resourceOffset: 0, data: heicData, range: 0 ..< heicData.count, complete: true))
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6) {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< data.count, complete: true))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
|
|
|
|||
20
submodules/MozjpegBinding/BUCK
Normal file
20
submodules/MozjpegBinding/BUCK
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "MozjpegBinding",
|
||||
srcs = glob([
|
||||
"Sources/*.m",
|
||||
]),
|
||||
headers = glob([
|
||||
"Sources/*.h",
|
||||
]),
|
||||
exported_headers = glob([
|
||||
"Public/**/*.h",
|
||||
]),
|
||||
deps = [
|
||||
"//third-party/mozjpeg:mozjpeg",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
],
|
||||
)
|
||||
22
submodules/MozjpegBinding/BUILD
Normal file
22
submodules/MozjpegBinding/BUILD
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
objc_library(
|
||||
name = "MozjpegBinding",
|
||||
module_name = "MozjpegBinding",
|
||||
enable_modules = True,
|
||||
srcs = glob([
|
||||
"Sources/**/*.m",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"Public/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"Public",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/mozjpeg:mozjpeg",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
|
||||
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
|
||||
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image);
|
||||
256
submodules/MozjpegBinding/Sources/MozjpegBinding.m
Normal file
256
submodules/MozjpegBinding/Sources/MozjpegBinding.m
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
#import <MozjpegBinding/MozjpegBinding.h>
|
||||
|
||||
#import <mozjpeg/turbojpeg.h>
|
||||
#import <mozjpeg/jpeglib.h>
|
||||
|
||||
static NSData *getHeaderPattern() {
|
||||
static NSData *value = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
value = [[NSData alloc] initWithBase64EncodedString:@"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCAAAAAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwA=" options:0];
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static NSData *getFooterPattern() {
|
||||
static NSData *value = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
value = [[NSData alloc] initWithBase64EncodedString:@"/9k=" options:0];
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data) {
|
||||
NSMutableArray<NSNumber *> *result = [[NSMutableArray alloc] init];
|
||||
|
||||
const uint8_t *dataBytes = data.bytes;
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
bool found = false;
|
||||
for (int i = offset + 2; i < data.length - 1; i++) {
|
||||
if (dataBytes[i] == 0xffU && dataBytes[i + 1] == 0xdaU) {
|
||||
if (offset != 0) {
|
||||
[result addObject:@(i)];
|
||||
}
|
||||
offset = i;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static NSString *sessionPrefix = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sessionPrefix = [NSString stringWithFormat:@"%u", arc4random()];
|
||||
});
|
||||
|
||||
NSString *randomId = [NSString stringWithFormat:@"%u", arc4random()];
|
||||
NSString *dirPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:sessionPrefix] stringByAppendingPathComponent:randomId];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:true attributes:nil error:nil];
|
||||
for (int i = 0; i < result.count + 1; i++) {
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%d.jpg", i]];
|
||||
if (i == result.count) {
|
||||
[data writeToFile:filePath atomically:true];
|
||||
} else {
|
||||
[[data subdataWithRange:NSMakeRange(0, [result[i] intValue])] writeToFile:filePath atomically:true];
|
||||
}
|
||||
}
|
||||
NSLog(@"Path: %@", dirPath);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) {
|
||||
int width = (int)(sourceImage.size.width * sourceImage.scale);
|
||||
int height = (int)(sourceImage.size.height * sourceImage.scale);
|
||||
|
||||
int targetBytesPerRow = ((4 * (int)width) + 15) & (~15);
|
||||
uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height));
|
||||
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host;
|
||||
|
||||
CGContextRef targetContext = CGBitmapContextCreate(targetMemory, width, height, 8, targetBytesPerRow, colorSpace, bitmapInfo);
|
||||
|
||||
UIGraphicsPushContext(targetContext);
|
||||
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
CGContextDrawImage(targetContext, CGRectMake(0, 0, width, height), sourceImage.CGImage);
|
||||
|
||||
UIGraphicsPopContext();
|
||||
|
||||
int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15);
|
||||
uint8_t *buffer = malloc(bufferBytesPerRow * height);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint32_t *color = ((uint32_t *)&targetMemory[y * targetBytesPerRow + x * 4]);
|
||||
|
||||
uint32_t r = ((*color >> 16) & 0xff);
|
||||
uint32_t g = ((*color >> 8) & 0xff);
|
||||
uint32_t b = (*color & 0xff);
|
||||
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 0] = r;
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 1] = g;
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 2] = b;
|
||||
}
|
||||
}
|
||||
|
||||
CGContextRelease(targetContext);
|
||||
|
||||
free(targetMemory);
|
||||
|
||||
struct jpeg_compress_struct cinfo;
|
||||
struct jpeg_error_mgr jerr;
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
jpeg_create_compress(&cinfo);
|
||||
|
||||
uint8_t *outBuffer = NULL;
|
||||
unsigned long outSize = 0;
|
||||
jpeg_mem_dest(&cinfo, &outBuffer, &outSize);
|
||||
|
||||
cinfo.image_width = (uint32_t)width;
|
||||
cinfo.image_height = (uint32_t)height;
|
||||
cinfo.input_components = 3;
|
||||
cinfo.in_color_space = JCS_RGB;
|
||||
jpeg_c_set_int_param(&cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST);
|
||||
jpeg_set_defaults(&cinfo);
|
||||
cinfo.arith_code = FALSE;
|
||||
cinfo.dct_method = JDCT_ISLOW;
|
||||
cinfo.optimize_coding = TRUE;
|
||||
jpeg_set_quality(&cinfo, 78, 1);
|
||||
jpeg_simple_progression(&cinfo);
|
||||
jpeg_start_compress(&cinfo, 1);
|
||||
|
||||
JSAMPROW rowPointer[1];
|
||||
while (cinfo.next_scanline < cinfo.image_height) {
|
||||
rowPointer[0] = (JSAMPROW)(buffer + cinfo.next_scanline * bufferBytesPerRow);
|
||||
jpeg_write_scanlines(&cinfo, rowPointer, 1);
|
||||
}
|
||||
|
||||
jpeg_finish_compress(&cinfo);
|
||||
|
||||
NSData *result = [[NSData alloc] initWithBytes:outBuffer length:outSize];
|
||||
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
|
||||
free(buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image) {
|
||||
CGSize size = CGSizeMake(40.0f, 40.0f);
|
||||
CGSize fittedSize = image.size;
|
||||
if (fittedSize.width > size.width) {
|
||||
fittedSize = CGSizeMake(size.width, (int)((fittedSize.height * size.width / MAX(fittedSize.width, 1.0f))));
|
||||
}
|
||||
if (fittedSize.height > size.height) {
|
||||
fittedSize = CGSizeMake((int)((fittedSize.width * size.height / MAX(fittedSize.height, 1.0f))), size.height);
|
||||
}
|
||||
|
||||
int width = (int)fittedSize.width;
|
||||
int height = (int)fittedSize.height;
|
||||
|
||||
int targetBytesPerRow = ((4 * (int)width) + 15) & (~15);
|
||||
uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height));
|
||||
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host;
|
||||
|
||||
CGContextRef targetContext = CGBitmapContextCreate(targetMemory, width, height, 8, targetBytesPerRow, colorSpace, bitmapInfo);
|
||||
|
||||
UIGraphicsPushContext(targetContext);
|
||||
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
CGContextDrawImage(targetContext, CGRectMake(0, 0, width, height), image.CGImage);
|
||||
|
||||
UIGraphicsPopContext();
|
||||
|
||||
int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15);
|
||||
uint8_t *buffer = malloc(bufferBytesPerRow * height);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint32_t *color = ((uint32_t *)&targetMemory[y * targetBytesPerRow + x * 4]);
|
||||
|
||||
uint32_t r = ((*color >> 16) & 0xff);
|
||||
uint32_t g = ((*color >> 8) & 0xff);
|
||||
uint32_t b = (*color & 0xff);
|
||||
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 0] = r;
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 1] = g;
|
||||
buffer[y * bufferBytesPerRow + x * 3 + 2] = b;
|
||||
}
|
||||
}
|
||||
|
||||
CGContextRelease(targetContext);
|
||||
|
||||
free(targetMemory);
|
||||
|
||||
struct jpeg_compress_struct cinfo;
|
||||
struct jpeg_error_mgr jerr;
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
jpeg_create_compress(&cinfo);
|
||||
|
||||
uint8_t *outBuffer = NULL;
|
||||
unsigned long outSize = 0;
|
||||
jpeg_mem_dest(&cinfo, &outBuffer, &outSize);
|
||||
|
||||
cinfo.image_width = (uint32_t)width;
|
||||
cinfo.image_height = (uint32_t)height;
|
||||
cinfo.input_components = 3;
|
||||
cinfo.in_color_space = JCS_RGB;
|
||||
jpeg_c_set_int_param(&cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST);
|
||||
jpeg_set_defaults(&cinfo);
|
||||
cinfo.arith_code = FALSE;
|
||||
cinfo.dct_method = JDCT_ISLOW;
|
||||
cinfo.optimize_coding = FALSE;
|
||||
jpeg_set_quality(&cinfo, 20, 1);
|
||||
jpeg_start_compress(&cinfo, 1);
|
||||
|
||||
JSAMPROW rowPointer[1];
|
||||
while (cinfo.next_scanline < cinfo.image_height) {
|
||||
rowPointer[0] = (JSAMPROW)(buffer + cinfo.next_scanline * bufferBytesPerRow);
|
||||
jpeg_write_scanlines(&cinfo, rowPointer, 1);
|
||||
}
|
||||
|
||||
jpeg_finish_compress(&cinfo);
|
||||
|
||||
NSMutableData *serializedData = nil;
|
||||
|
||||
NSData *headerPattern = getHeaderPattern();
|
||||
NSData *footerPattern = getFooterPattern();
|
||||
if (outBuffer[164] == height && outBuffer[166] == width && headerPattern != nil && footerPattern != nil) {
|
||||
outBuffer[164] = 0;
|
||||
outBuffer[166] = 0;
|
||||
|
||||
if (memcmp(headerPattern.bytes, outBuffer, headerPattern.length) == 0) {
|
||||
if (memcmp(footerPattern.bytes, outBuffer + outSize - footerPattern.length, footerPattern.length) == 0) {
|
||||
serializedData = [[NSMutableData alloc] init];
|
||||
uint8_t version = 1;
|
||||
[serializedData appendBytes:&version length:1];
|
||||
uint8_t outWidth = (uint8_t)width;
|
||||
uint8_t outHeight = (uint8_t)height;
|
||||
[serializedData appendBytes:&outHeight length:1];
|
||||
[serializedData appendBytes:&outWidth length:1];
|
||||
unsigned long contentSize = outSize - headerPattern.length - footerPattern.length;
|
||||
[serializedData appendBytes:outBuffer + headerPattern.length length:contentSize];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
|
||||
free(buffer);
|
||||
|
||||
return serializedData;
|
||||
}
|
||||
|
|
@ -700,14 +700,14 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
|
|||
case .emailConfirmation:
|
||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") {
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct)
|
||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
animatedStickerNode.visibility = true
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
}
|
||||
case .passwordHint:
|
||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupHint", ofType: "tgs") {
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct)
|
||||
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
animatedStickerNode.visibility = true
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode {
|
|||
buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action
|
||||
|
||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupIntro", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode {
|
|||
buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action
|
||||
|
||||
if let path = getAppBundle().path(forResource: "TwoFactorSetupDone", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -766,11 +766,6 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
}))
|
||||
}
|
||||
|
||||
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ProfilePhoto_OpenInEditor, color: .accent, action: { [weak self] in
|
||||
// dismissAction()
|
||||
// self?.openEntryEdit(rawEntry)
|
||||
// }))
|
||||
|
||||
let deleteTitle: String
|
||||
if let _ = rawEntry.videoRepresentations.last {
|
||||
deleteTitle = self.presentationData.strings.Settings_RemoveVideo
|
||||
|
|
|
|||
|
|
@ -734,7 +734,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
|
|||
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [])
|
||||
updateState {
|
||||
$0.withUpdatedUpdatingAvatar(.image(representation, true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -877,7 +877,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
|
||||
dismissAction()
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: user.id, isVideo: false, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: user.id, isVideo: false, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == user.id {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -888,7 +888,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||
} |> deliverOnMainQueue).start(next: { peer, current in
|
||||
if let peer = peer, let current = current {
|
||||
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: false, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peer.id, isVideo: false, endCurrentIfAny: true)
|
||||
})]), nil)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1460,7 +1460,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
|
|||
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [])
|
||||
updateState {
|
||||
$0.withUpdatedUpdatingAvatar(.image(representation, true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat
|
|||
|
||||
var callsAvailable = true
|
||||
if let cachedUserData = cachedPeerData as? CachedUserData {
|
||||
callsAvailable = cachedUserData.callsAvailable
|
||||
callsAvailable = cachedUserData.voiceCallsAvailable
|
||||
}
|
||||
|
||||
entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: user, presence: view.peerPresences[user.id], cachedData: cachedPeerData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: user.botInfo == nil && callsAvailable))
|
||||
|
|
@ -873,7 +873,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peer.id {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -884,7 +884,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
} |> deliverOnMainQueue).start(next: { peer, current in
|
||||
if let peer = peer, let current = current {
|
||||
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: true)
|
||||
})]), nil)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode {
|
|||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Compass", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,110 @@ private enum ResourceFileData {
|
|||
}
|
||||
|
||||
public func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMediaImageRepresentation? {
|
||||
if let progressiveRepresentation = progressiveImageRepresentation(photo.representations) {
|
||||
return progressiveRepresentation
|
||||
}
|
||||
return photo.representationForDisplayAtSize(PixelDimensions(width: 1280, height: 1280))
|
||||
}
|
||||
|
||||
private let progressiveRangeMap: [(Int, [Int])] = [
|
||||
(100, [0]),
|
||||
(400, [2]),
|
||||
(600, [4, 5]),
|
||||
(Int(Int32.max), [4, 5, 8, 9])
|
||||
]
|
||||
|
||||
public func representationFetchRangeForDisplayAtSize(representation: TelegramMediaImageRepresentation, dimension: Int) -> Range<Int>? {
|
||||
if representation.progressiveSizes.count > 1 {
|
||||
var largestByteSize = Int(representation.progressiveSizes[0])
|
||||
for (maxDimension, byteSizes) in progressiveRangeMap {
|
||||
largestByteSize = Int(representation.progressiveSizes[byteSizes.last!])
|
||||
if maxDimension >= dimension {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0 ..< largestByteSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false) -> Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> {
|
||||
if let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count == 10 {
|
||||
enum SizeSource {
|
||||
case miniThumbnail(data: Data)
|
||||
case image(size: Int)
|
||||
}
|
||||
|
||||
var sources: [SizeSource] = []
|
||||
if let miniThumbnail = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) {
|
||||
sources.append(.miniThumbnail(data: miniThumbnail))
|
||||
}
|
||||
let thumbnailByteSize = Int(progressiveRepresentation.progressiveSizes[0])
|
||||
var largestByteSize = Int(progressiveRepresentation.progressiveSizes[0])
|
||||
for (maxDimension, byteSizes) in progressiveRangeMap {
|
||||
if Int(fullRepresentationSize.width) > 100 && maxDimension <= 100 {
|
||||
continue
|
||||
}
|
||||
sources.append(contentsOf: byteSizes.map { sizeIndex -> SizeSource in
|
||||
return .image(size: Int(progressiveRepresentation.progressiveSizes[sizeIndex]))
|
||||
})
|
||||
largestByteSize = Int(progressiveRepresentation.progressiveSizes[byteSizes.last!])
|
||||
if maxDimension >= Int(fullRepresentationSize.width) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
let signals: [Signal<(SizeSource, Data?), NoError>] = sources.map { source -> Signal<(SizeSource, Data?), NoError> in
|
||||
switch source {
|
||||
case let .miniThumbnail(data):
|
||||
return .single((source, data))
|
||||
case let .image(size):
|
||||
return postbox.mediaBox.resourceData(progressiveRepresentation.resource, size: Int(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad)
|
||||
|> map { (data, _) -> (SizeSource, Data?) in
|
||||
return (source, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dataDisposable = combineLatest(signals).start(next: { results in
|
||||
var foundData = false
|
||||
loop: for i in (0 ..< results.count).reversed() {
|
||||
let isLastSize = i == results.count - 1
|
||||
switch results[i].0 {
|
||||
case .image:
|
||||
if let data = results[i].1, data.count != 0 {
|
||||
subscriber.putNext(Tuple4(nil, data, .full, isLastSize))
|
||||
foundData = true
|
||||
if isLastSize {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
break loop
|
||||
}
|
||||
case let .miniThumbnail(thumbnailData):
|
||||
subscriber.putNext(Tuple4(thumbnailData, nil, .blurred, false))
|
||||
foundData = true
|
||||
break loop
|
||||
}
|
||||
}
|
||||
if !foundData {
|
||||
subscriber.putNext(Tuple4(nil, nil, .blurred, false))
|
||||
}
|
||||
})
|
||||
var fetchDisposable: Disposable?
|
||||
if autoFetchFullSize {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< largestByteSize, .default), statsCategory: .image).start()
|
||||
} else if useMiniThumbnailIfAvailable {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< thumbnailByteSize, .default), statsCategory: .image).start()
|
||||
}
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
fetchDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) {
|
||||
let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)
|
||||
let maybeLargestSize = postbox.mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)
|
||||
|
|
@ -1158,7 +1258,9 @@ public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaRe
|
|||
let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0
|
||||
var updatedFullRepresentationSize = fullRepresentationSize
|
||||
if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) {
|
||||
updatedFullRepresentationSize = largest.dimensions.cgSize
|
||||
if progressiveImageRepresentation(photoReference.media.representations) == nil {
|
||||
updatedFullRepresentationSize = largest.dimensions.cgSize
|
||||
}
|
||||
}
|
||||
let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable)
|
||||
|
||||
|
|
@ -1541,9 +1643,31 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File
|
|||
}
|
||||
}
|
||||
|
||||
public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId, photoReference: ImageMediaReference) -> Signal<MediaResourceStatus, NoError> {
|
||||
public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId, photoReference: ImageMediaReference, displayAtSize: Int = 1000) -> Signal<MediaResourceStatus, NoError> {
|
||||
if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) {
|
||||
return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource)
|
||||
if let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) {
|
||||
return combineLatest(
|
||||
context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource),
|
||||
context.account.postbox.mediaBox.resourceRangesStatus(largestRepresentation.resource)
|
||||
)
|
||||
|> map { status, rangeStatus -> MediaResourceStatus in
|
||||
if rangeStatus.contains(integersIn: range) {
|
||||
return .Local
|
||||
}
|
||||
|
||||
switch status {
|
||||
case .Local:
|
||||
return .Local
|
||||
case .Remote:
|
||||
return .Remote
|
||||
case let .Fetching(isActive, progress):
|
||||
return .Fetching(isActive: isActive, progress: max(progress, 0.0))
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource)
|
||||
}
|
||||
} else {
|
||||
return .never()
|
||||
}
|
||||
|
|
@ -1560,15 +1684,19 @@ public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photo
|
|||
}
|
||||
}
|
||||
|
||||
public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||
public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int = 1000, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||
if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) {
|
||||
return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true)
|
||||
var fetchRange: (Range<Int>, MediaBoxFetchPriority)?
|
||||
if let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) {
|
||||
fetchRange = (range, .default)
|
||||
}
|
||||
|
||||
return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), range: fetchRange, statsCategory: .image, reportResultStatus: true)
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if case .remote = type, let peerType = storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType)
|
||||
|> castError(FetchResourceError.self)
|
||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
return .complete()
|
||||
}
|
||||
|> then(.single(type))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,11 @@ public final class MediaBox {
|
|||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)"
|
||||
}
|
||||
|
||||
public func shortLivedResourceCachePathPrefix(_ id: MediaResourceId) -> String {
|
||||
let cacheString = "short-cache"
|
||||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id))"
|
||||
}
|
||||
|
||||
public func storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool = false) {
|
||||
let begin = {
|
||||
let paths = self.storePathsForId(id)
|
||||
|
|
@ -593,10 +598,40 @@ public final class MediaBox {
|
|||
}
|
||||
}
|
||||
|
||||
public func resourceData(_ resource: MediaResource, size: Int, in range: Range<Int>, mode: ResourceDataRangeMode = .complete) -> Signal<(Data, Bool), NoError> {
|
||||
public func resourceData(_ resource: MediaResource, size: Int, in range: Range<Int>, mode: ResourceDataRangeMode = .complete, notifyAboutIncomplete: Bool = false, attemptSynchronously: Bool = false) -> Signal<(Data, Bool), NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
if attemptSynchronously {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
|
||||
if let completeSize = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete
|
||||
])
|
||||
|
||||
if let file = ManagedFile(queue: nil, path: paths.complete, mode: .read) {
|
||||
let clippedLowerBound = min(completeSize, max(0, range.lowerBound))
|
||||
let clippedUpperBound = min(completeSize, max(0, range.upperBound))
|
||||
if clippedLowerBound < clippedUpperBound {
|
||||
file.seek(position: Int64(clippedLowerBound))
|
||||
let data = file.readData(count: clippedUpperBound)
|
||||
subscriber.putNext((data, true))
|
||||
} else {
|
||||
subscriber.putNext((Data(), isComplete: true))
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
} else {
|
||||
if let data = MediaBoxPartialFile.extractPartialData(path: paths.partial, metaPath: paths.partial + ".meta", range: Int32(range.lowerBound) ..< Int32(range.upperBound)) {
|
||||
subscriber.putNext((data, true))
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dataQueue.async {
|
||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource) else {
|
||||
subscriber.putCompletion()
|
||||
|
|
@ -608,24 +643,25 @@ public final class MediaBox {
|
|||
let dataDisposable = fileContext.data(range: range, waitUntilAfterInitialFetch: false, next: { result in
|
||||
if let file = ManagedFile(queue: self.dataQueue, path: result.path, mode: .read), let fileSize = file.getSize() {
|
||||
if result.complete {
|
||||
if result.offset + result.size <= fileSize {
|
||||
if fileSize >= result.offset + result.size {
|
||||
file.seek(position: Int64(result.offset))
|
||||
let resultData = file.readData(count: result.size)
|
||||
subscriber.putNext((resultData, true))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
assertionFailure("data.count >= result.offset + result.size")
|
||||
}
|
||||
let clippedLowerBound = min(result.offset, fileSize)
|
||||
let clippedUpperBound = min(result.offset + result.size, fileSize)
|
||||
if clippedUpperBound == clippedLowerBound {
|
||||
subscriber.putNext((Data(), true))
|
||||
subscriber.putCompletion()
|
||||
} else if clippedUpperBound <= fileSize {
|
||||
file.seek(position: Int64(clippedLowerBound))
|
||||
let resultData = file.readData(count: clippedUpperBound - clippedLowerBound)
|
||||
subscriber.putNext((resultData, true))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
} else {
|
||||
switch mode {
|
||||
case .complete:
|
||||
break
|
||||
case .incremental:
|
||||
break
|
||||
case .complete, .incremental:
|
||||
if notifyAboutIncomplete {
|
||||
subscriber.putNext((Data(), false))
|
||||
}
|
||||
case .partial:
|
||||
subscriber.putNext((Data(), false))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,6 +216,23 @@ final class MediaBoxPartialFile {
|
|||
self.currentFetch?.1.dispose()
|
||||
}
|
||||
|
||||
static func extractPartialData(path: String, metaPath: String, range: Range<Int32>) -> Data? {
|
||||
guard let metadataFd = ManagedFile(queue: nil, path: metaPath, mode: .read) else {
|
||||
return nil
|
||||
}
|
||||
guard let fd = ManagedFile(queue: nil, path: path, mode: .read) else {
|
||||
return nil
|
||||
}
|
||||
guard let fileMap = MediaBoxFileMap(fd: metadataFd) else {
|
||||
return nil
|
||||
}
|
||||
guard let clippedRange = fileMap.contains(range) else {
|
||||
return nil
|
||||
}
|
||||
fd.seek(position: Int64(clippedRange.lowerBound))
|
||||
return fd.readData(count: Int(clippedRange.upperBound - clippedRange.lowerBound))
|
||||
}
|
||||
|
||||
var storedSize: Int32 {
|
||||
assert(self.queue.isCurrent())
|
||||
return self.fileMap.sum
|
||||
|
|
@ -582,7 +599,7 @@ final class MediaBoxPartialFile {
|
|||
|
||||
private func immediateStatus(size: Int32?) -> MediaResourceStatus {
|
||||
let status: MediaResourceStatus
|
||||
if self.fullRangeRequests.isEmpty {
|
||||
if self.fullRangeRequests.isEmpty && self.currentFetch == nil {
|
||||
if let truncationSize = self.fileMap.truncationSize, self.fileMap.sum == truncationSize {
|
||||
status = .Local
|
||||
} else {
|
||||
|
|
@ -633,6 +650,7 @@ final class MediaBoxPartialFile {
|
|||
if intervals.isEmpty {
|
||||
if let (_, disposable) = self.currentFetch {
|
||||
self.currentFetch = nil
|
||||
self.updateStatuses()
|
||||
disposable.dispose()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -642,6 +660,7 @@ final class MediaBoxPartialFile {
|
|||
let promise = Promise<[(Range<Int>, MediaBoxFetchPriority)]>()
|
||||
let disposable = MetaDisposable()
|
||||
self.currentFetch = (promise, disposable)
|
||||
self.updateStatuses()
|
||||
disposable.set((fetch(promise.get())
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] data in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,21 @@ import AsyncDisplayKit
|
|||
import Display
|
||||
|
||||
public enum SemanticStatusNodeState: Equatable {
|
||||
public struct ProgressAppearance: Equatable {
|
||||
public var inset: CGFloat
|
||||
public var lineWidth: CGFloat
|
||||
|
||||
public init(inset: CGFloat, lineWidth: CGFloat) {
|
||||
self.inset = inset
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
}
|
||||
|
||||
case none
|
||||
case download
|
||||
case play
|
||||
case pause
|
||||
case progress(value: CGFloat?, cancelEnabled: Bool)
|
||||
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
||||
case customIcon(UIImage)
|
||||
}
|
||||
|
||||
|
|
@ -224,12 +234,14 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
|||
let transitionFraction: CGFloat
|
||||
let value: CGFloat?
|
||||
let displayCancel: Bool
|
||||
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
||||
let timestamp: Double
|
||||
|
||||
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, timestamp: Double) {
|
||||
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) {
|
||||
self.transitionFraction = transitionFraction
|
||||
self.value = value
|
||||
self.displayCancel = displayCancel
|
||||
self.appearance = appearance
|
||||
self.timestamp = timestamp
|
||||
|
||||
super.init()
|
||||
|
|
@ -252,22 +264,49 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
|||
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
||||
}
|
||||
|
||||
var progress = self.value ?? 0.1
|
||||
var startAngle = -CGFloat.pi / 2.0
|
||||
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||
|
||||
if progress > 1.0 {
|
||||
progress = 2.0 - progress
|
||||
let tmp = startAngle
|
||||
startAngle = endAngle
|
||||
endAngle = tmp
|
||||
var progress: CGFloat
|
||||
var startAngle: CGFloat
|
||||
var endAngle: CGFloat
|
||||
if let value = self.value {
|
||||
progress = value
|
||||
startAngle = -CGFloat.pi / 2.0
|
||||
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||
|
||||
if progress > 1.0 {
|
||||
progress = 2.0 - progress
|
||||
let tmp = startAngle
|
||||
startAngle = endAngle
|
||||
endAngle = tmp
|
||||
}
|
||||
progress = min(1.0, progress)
|
||||
} else {
|
||||
progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0))
|
||||
|
||||
startAngle = -CGFloat.pi / 2.0
|
||||
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||
|
||||
if progress > 1.0 {
|
||||
progress = 2.0 - progress
|
||||
let tmp = startAngle
|
||||
startAngle = endAngle
|
||||
endAngle = tmp
|
||||
}
|
||||
progress = min(1.0, progress)
|
||||
}
|
||||
progress = min(1.0, progress)
|
||||
|
||||
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
|
||||
let lineWidth: CGFloat
|
||||
if let appearance = self.appearance {
|
||||
lineWidth = appearance.lineWidth
|
||||
} else {
|
||||
lineWidth = max(1.6, 2.25 * factor)
|
||||
}
|
||||
|
||||
let pathDiameter: CGFloat
|
||||
pathDiameter = diameter - lineWidth - 2.5 * 2.0
|
||||
if let appearance = self.appearance {
|
||||
pathDiameter = diameter - lineWidth - appearance.inset * 2.0
|
||||
} else {
|
||||
pathDiameter = diameter - lineWidth - 2.5 * 2.0
|
||||
}
|
||||
|
||||
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
|
||||
angle *= 4.0
|
||||
|
|
@ -317,15 +356,17 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
|||
|
||||
var value: CGFloat?
|
||||
let displayCancel: Bool
|
||||
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
||||
var transition: SemanticStatusNodeProgressTransition?
|
||||
|
||||
var isAnimating: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(value: CGFloat?, displayCancel: Bool) {
|
||||
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
|
||||
self.value = value
|
||||
self.displayCancel = displayCancel
|
||||
self.appearance = appearance
|
||||
}
|
||||
|
||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
||||
|
|
@ -341,7 +382,7 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
|||
} else {
|
||||
resolvedValue = nil
|
||||
}
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, timestamp: timestamp)
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
|
||||
}
|
||||
|
||||
func updateValue(value: CGFloat?) {
|
||||
|
|
@ -386,12 +427,12 @@ private extension SemanticStatusNodeState {
|
|||
} else {
|
||||
return SemanticStatusNodeIconContext(icon: icon)
|
||||
}
|
||||
case let .progress(value, cancelEnabled):
|
||||
case let .progress(value, cancelEnabled, appearance):
|
||||
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
||||
current.updateValue(value: value)
|
||||
return current
|
||||
} else {
|
||||
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled)
|
||||
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
case alternativeFolderTabs(Bool)
|
||||
case playerEmbedding(Bool)
|
||||
case playlistPlayback(Bool)
|
||||
case videoCalls(Bool)
|
||||
case videoCallsReference(Bool)
|
||||
case videoCallsInfo(PresentationTheme, String)
|
||||
case enableHighBitrateVideoCalls(Bool)
|
||||
case hostInfo(PresentationTheme, String)
|
||||
case versionInfo(PresentationTheme)
|
||||
|
||||
|
|
@ -90,7 +88,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .videoCalls, .videoCallsReference, .videoCallsInfo:
|
||||
case .enableHighBitrateVideoCalls:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
case .hostInfo, .versionInfo:
|
||||
return DebugControllerSection.info.rawValue
|
||||
|
|
@ -149,12 +147,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return 24
|
||||
case .playlistPlayback:
|
||||
return 25
|
||||
case .videoCalls:
|
||||
case .enableHighBitrateVideoCalls:
|
||||
return 26
|
||||
case .videoCallsReference:
|
||||
return 27
|
||||
case .videoCallsInfo:
|
||||
return 28
|
||||
case .hostInfo:
|
||||
return 29
|
||||
case .versionInfo:
|
||||
|
|
@ -576,28 +570,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
})
|
||||
}).start()
|
||||
})
|
||||
case let .videoCalls(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Feature", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
case let .enableHighBitrateVideoCalls(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "HD Video Calls", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
|
||||
settings.videoCalls = value
|
||||
settings.enableHighBitrateVideoCalls = value
|
||||
return settings
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .videoCallsReference(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Reference Implementation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
|
||||
settings.videoCallsReference = value
|
||||
return settings
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .videoCallsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .hostInfo(theme, string):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section)
|
||||
case let .versionInfo(theme):
|
||||
|
|
@ -643,11 +625,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS
|
|||
entries.append(.alternativeFolderTabs(experimentalSettings.foldersTabAtBottom))
|
||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
entries.append(.videoCalls(experimentalSettings.videoCalls))
|
||||
if experimentalSettings.videoCalls {
|
||||
entries.append(.videoCallsReference(experimentalSettings.videoCallsReference))
|
||||
}
|
||||
entries.append(.videoCallsInfo(presentationData.theme, "Enables experimental transmission of electromagnetic radiation synchronized with pressure waves. Needs to be enabled on both sides."))
|
||||
entries.append(.enableHighBitrateVideoCalls(experimentalSettings.enableHighBitrateVideoCalls))
|
||||
|
||||
if let backupHostOverride = networkSettings?.backupHostOverride {
|
||||
entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)"))
|
||||
|
|
|
|||
|
|
@ -998,7 +998,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
|||
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [])
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.updatingAvatar = .image(representation, true)
|
||||
|
|
@ -1025,7 +1025,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
|||
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||
let photoResource = LocalFileMediaResource(fileId: arc4random64())
|
||||
context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [])
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.updatingAvatar = .image(representation, true)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE
|
|||
|
||||
let apply: () -> Void = {
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), color: nil, intensity: nil)
|
||||
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource)], settings)
|
||||
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: []), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [])], settings)
|
||||
updateWallpaper(wallpaper)
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll
|
|||
|> mapToSignal { wallpaper -> Signal<TelegramWallpaper?, NoError> in
|
||||
if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper {
|
||||
var convertedRepresentations: [ImageRepresentationWithReference] = []
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
return wallpaperDatas(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|
||||
|> mapToSignal { _, fullSizeData, complete -> Signal<TelegramWallpaper?, NoError> in
|
||||
guard complete, let fullSizeData = fullSizeData else {
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
|||
for representation in file.file.previewRepresentations {
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource)))
|
||||
}
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
|
||||
wallpaperSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true)
|
||||
} else if let bottomColor = backgroundColors.1 {
|
||||
|
|
|
|||
|
|
@ -109,10 +109,10 @@ final class ThemeGridSearchItemNode: GridItemNode {
|
|||
|
||||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: []))
|
||||
}
|
||||
if let imageResource = imageResource, let imageDimensions = imageDimensions {
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: []))
|
||||
}
|
||||
if !representations.isEmpty {
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
for representation in file.file.previewRepresentations {
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource)))
|
||||
}
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
|
||||
let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
|
||||
let fileReference = FileMediaReference.standalone(media: file.file)
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
for representation in file.file.previewRepresentations {
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: representation.resource, media: file.file, message: message)))
|
||||
}
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: reference(for: file.file.resource, media: file.file, message: message)))
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: []), reference: reference(for: file.file.resource, media: file.file, message: message)))
|
||||
|
||||
if wallpaper.isPattern {
|
||||
var patternColors: [UIColor] = []
|
||||
|
|
@ -448,9 +448,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
|
||||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: []))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: []))
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
|
||||
signal = chatMessagePhoto(postbox: context.account.postbox, photoReference: .standalone(media: tmpImage))
|
||||
|
|
|
|||
|
|
@ -290,6 +290,8 @@ public final class ShareController: ViewController {
|
|||
private let presetText: String?
|
||||
private let switchableAccounts: [AccountWithInfo]
|
||||
private let immediatePeerId: PeerId?
|
||||
private let openStats: (() -> Void)?
|
||||
private let shares: Int?
|
||||
|
||||
private let peers = Promise<([(RenderedPeer, PeerPresence?)], Peer)>()
|
||||
private let peersDisposable = MetaDisposable()
|
||||
|
|
@ -300,11 +302,11 @@ public final class ShareController: ViewController {
|
|||
|
||||
public var dismissed: ((Bool) -> Void)?
|
||||
|
||||
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
|
||||
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
|
||||
}
|
||||
|
||||
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||
self.sharedContext = sharedContext
|
||||
self.currentContext = currentContext
|
||||
self.currentAccount = currentContext.account
|
||||
|
|
@ -314,6 +316,8 @@ public final class ShareController: ViewController {
|
|||
self.immediateExternalShare = immediateExternalShare
|
||||
self.switchableAccounts = switchableAccounts
|
||||
self.immediatePeerId = immediatePeerId
|
||||
self.openStats = openStats
|
||||
self.shares = shares
|
||||
|
||||
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
|
|
@ -437,7 +441,7 @@ public final class ShareController: ViewController {
|
|||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId)
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares)
|
||||
self.controllerNode.dismiss = { [weak self] shared in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
self?.dismissed?(shared)
|
||||
|
|
@ -714,6 +718,11 @@ public final class ShareController: ViewController {
|
|||
strongSelf.view.endEditing(true)
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
if case .messages = self.subject, let openStats = self.openStats {
|
||||
self.controllerNode.openStats = {
|
||||
openStats()
|
||||
}
|
||||
}
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.peersDisposable.set((self.peers.get()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
private let externalShare: Bool
|
||||
private let immediateExternalShare: Bool
|
||||
private var immediatePeerId: PeerId?
|
||||
private let shares: Int?
|
||||
|
||||
private let defaultAction: ShareControllerAction?
|
||||
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
|
|
@ -61,6 +62,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
var share: ((String, [PeerId]) -> Signal<ShareState, NoError>)?
|
||||
var shareExternal: (() -> Signal<ShareExternalState, NoError>)?
|
||||
var switchToAnotherAccount: (() -> Void)?
|
||||
var openStats: (() -> Void)?
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
private var didSetReady = false
|
||||
|
|
@ -78,12 +80,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
|
||||
private let presetText: String?
|
||||
|
||||
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) {
|
||||
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?) {
|
||||
self.sharedContext = sharedContext
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.externalShare = externalShare
|
||||
self.immediateExternalShare = immediateExternalShare
|
||||
self.immediatePeerId = immediatePeerId
|
||||
self.shares = shares
|
||||
self.presentError = presentError
|
||||
|
||||
self.presetText = presetText
|
||||
|
|
@ -670,7 +673,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
let animated = self.peersContentNode == nil
|
||||
let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in
|
||||
self?.switchToAnotherAccount?()
|
||||
}, extendedInitialReveal: self.presetText != nil)
|
||||
}, extendedInitialReveal: self.presetText != nil, statsCount: self.shares ?? 0)
|
||||
self.peersContentNode = peersContentNode
|
||||
peersContentNode.openSearch = { [weak self] in
|
||||
let _ = (recentlySearchedPeers(postbox: context.account.postbox)
|
||||
|
|
@ -736,6 +739,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
peersContentNode.openShare = {
|
||||
openShare(false)
|
||||
}
|
||||
if let openStats = self.openStats {
|
||||
peersContentNode.openStats = { [weak self] in
|
||||
openStats()
|
||||
self?.animateOut(shared: true, completion: {
|
||||
self?.dismiss?(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
if self.immediateExternalShare {
|
||||
openShare(true)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
private let controllerInteraction: ShareControllerInteraction
|
||||
private let switchToAnotherAccount: () -> Void
|
||||
private let extendedInitialReveal: Bool
|
||||
private let statsCount: Int?
|
||||
|
||||
let accountPeer: Peer
|
||||
private let foundPeers = Promise<[RenderedPeer]>([])
|
||||
|
|
@ -96,11 +97,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
private let contentSeparatorNode: ASDisplayNode
|
||||
private let searchButtonNode: HighlightableButtonNode
|
||||
private let shareButtonNode: HighlightableButtonNode
|
||||
private let statsButtonNode: HighlightableButtonNode
|
||||
|
||||
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
var openSearch: (() -> Void)?
|
||||
var openShare: (() -> Void)?
|
||||
var openStats: (() -> Void)?
|
||||
|
||||
private var ensurePeerVisibleOnLayout: PeerId?
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
|
|
@ -108,7 +111,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
|
||||
let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>()
|
||||
|
||||
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool) {
|
||||
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool, statsCount: Int?) {
|
||||
self.sharedContext = sharedContext
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -118,6 +121,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.accountPeer = accountPeer
|
||||
self.switchToAnotherAccount = switchToAnotherAccount
|
||||
self.extendedInitialReveal = extendedInitialReveal
|
||||
self.statsCount = statsCount
|
||||
|
||||
self.peersValue.set(.single(peers))
|
||||
|
||||
|
|
@ -174,6 +178,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.shareButtonNode = HighlightableButtonNode()
|
||||
self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: [])
|
||||
|
||||
self.statsButtonNode = HighlightableButtonNode()
|
||||
self.statsButtonNode.setAttributedTitle(NSAttributedString(string: "\(statsCount ?? 0) Shares", font: Font.regular(17.0), textColor: self.theme.actionSheet.controlAccentColor), for: .normal)
|
||||
self.statsButtonNode.isHidden = statsCount == nil
|
||||
|
||||
self.contentTitleNode.isHidden = !self.statsButtonNode.isHidden
|
||||
self.contentSubtitleNode.isHidden = !self.statsButtonNode.isHidden
|
||||
|
||||
self.contentSeparatorNode = ASDisplayNode()
|
||||
self.contentSeparatorNode.isLayerBacked = true
|
||||
self.contentSeparatorNode.displaysAsynchronously = false
|
||||
|
|
@ -192,6 +203,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.addSubnode(self.contentTitleAccountNode)
|
||||
self.addSubnode(self.searchButtonNode)
|
||||
self.addSubnode(self.shareButtonNode)
|
||||
self.addSubnode(self.statsButtonNode)
|
||||
self.addSubnode(self.contentSeparatorNode)
|
||||
|
||||
let previousItems = Atomic<[SharePeerEntry]?>(value: [])
|
||||
|
|
@ -213,6 +225,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
|
||||
self.searchButtonNode.addTarget(self, action: #selector(self.searchPressed), forControlEvents: .touchUpInside)
|
||||
self.shareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
|
||||
self.statsButtonNode.addTarget(self, action: #selector(self.statsPressed), forControlEvents: .touchUpInside)
|
||||
self.contentTitleAccountNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.accountTapGesture(_:))))
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +354,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.contentSubtitleNode.frame = originalSubtitleFrame
|
||||
transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame)
|
||||
|
||||
let statsSize = self.statsButtonNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: titleAreaHeight))
|
||||
transition.updateFrame(node: self.statsButtonNode, frame: CGRect(origin: CGPoint(x: floor((size.width - statsSize.width) / 2.0), y: titleOffset + 22.0), size: statsSize))
|
||||
|
||||
let titleButtonSize = CGSize(width: 44.0, height: 44.0)
|
||||
let searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: titleOffset + 12.0), size: titleButtonSize)
|
||||
transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame)
|
||||
|
|
@ -376,25 +392,34 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
|
||||
func updateSelectedPeers() {
|
||||
var subtitleText = self.strings.ShareMenu_SelectChats
|
||||
if !self.controllerInteraction.selectedPeers.isEmpty {
|
||||
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
|
||||
let text: String
|
||||
if peer.peerId == self.accountPeer.id {
|
||||
text = self.strings.DialogList_SavedMessages
|
||||
} else {
|
||||
text = peer.chatMainPeer?.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) ?? ""
|
||||
}
|
||||
|
||||
if !string.isEmpty {
|
||||
return string + ", " + text
|
||||
} else {
|
||||
return string + text
|
||||
}
|
||||
})
|
||||
if let _ = self.openStats, self.controllerInteraction.selectedPeers.isEmpty {
|
||||
self.statsButtonNode.isHidden = false
|
||||
self.contentTitleNode.isHidden = true
|
||||
self.contentSubtitleNode.isHidden = true
|
||||
} else {
|
||||
self.statsButtonNode.isHidden = true
|
||||
self.contentTitleNode.isHidden = false
|
||||
self.contentSubtitleNode.isHidden = false
|
||||
|
||||
var subtitleText = self.strings.ShareMenu_SelectChats
|
||||
if !self.controllerInteraction.selectedPeers.isEmpty {
|
||||
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
|
||||
let text: String
|
||||
if peer.peerId == self.accountPeer.id {
|
||||
text = self.strings.DialogList_SavedMessages
|
||||
} else {
|
||||
text = peer.chatMainPeer?.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) ?? ""
|
||||
}
|
||||
|
||||
if !string.isEmpty {
|
||||
return string + ", " + text
|
||||
} else {
|
||||
return string + text
|
||||
}
|
||||
})
|
||||
}
|
||||
self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||
}
|
||||
self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
|
||||
itemNode.updateSelection(animated: true)
|
||||
|
|
@ -410,6 +435,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.openShare?()
|
||||
}
|
||||
|
||||
@objc func statsPressed() {
|
||||
self.openStats?()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode, self.contentTitleAccountNode]
|
||||
for node in nodes {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
|||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import MapKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
|||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import MapKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
|
|
|
|||
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import ItemListUI
|
||||
import ItemListPeerItem
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import AppBundle
|
||||
import GraphUI
|
||||
|
||||
private final class MessageStatsControllerArguments {
|
||||
let context: AccountContext
|
||||
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
|
||||
let openMessage: (MessageId) -> Void
|
||||
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void) {
|
||||
self.context = context
|
||||
self.loadDetailedGraph = loadDetailedGraph
|
||||
self.openMessage = openMessage
|
||||
}
|
||||
}
|
||||
|
||||
private enum StatsSection: Int32 {
|
||||
case overview
|
||||
case interactions
|
||||
case publicForwards
|
||||
}
|
||||
|
||||
private enum StatsEntry: ItemListNodeEntry {
|
||||
case overviewTitle(PresentationTheme, String)
|
||||
case overview(PresentationTheme, MessageStats, Int32?)
|
||||
|
||||
case interactionsTitle(PresentationTheme, String)
|
||||
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
|
||||
|
||||
case publicForwardsTitle(PresentationTheme, String)
|
||||
case publicForward(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .overviewTitle, .overview:
|
||||
return StatsSection.overview.rawValue
|
||||
case .interactionsTitle, .interactionsGraph:
|
||||
return StatsSection.interactions.rawValue
|
||||
case .publicForwardsTitle, .publicForward:
|
||||
return StatsSection.publicForwards.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .overviewTitle:
|
||||
return 0
|
||||
case .overview:
|
||||
return 1
|
||||
case .interactionsTitle:
|
||||
return 2
|
||||
case .interactionsGraph:
|
||||
return 3
|
||||
case .publicForwardsTitle:
|
||||
return 4
|
||||
case let .publicForward(index, _, _, _, _):
|
||||
return 5 + index
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .overviewTitle(lhsTheme, lhsText):
|
||||
if case let .overviewTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText{
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .overview(lhsTheme, lhsStats, lhsPublicShares):
|
||||
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsPublicShares == rhsPublicShares {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .interactionsTitle(lhsTheme, lhsText):
|
||||
if case let .interactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .interactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType):
|
||||
if case let .interactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .publicForwardsTitle(lhsTheme, lhsText):
|
||||
if case let .publicForwardsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .publicForward(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage):
|
||||
if case let .publicForward(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! MessageStatsControllerArguments
|
||||
switch self {
|
||||
case let .overviewTitle(_, text),
|
||||
let .interactionsTitle(_, text),
|
||||
let .publicForwardsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .overview(_, stats, publicShares):
|
||||
return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
|
||||
case let .interactionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||
completion(data)
|
||||
}
|
||||
})
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
case let .publicForward(_, _, _, _, message):
|
||||
var views: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||
views = viewsAttribute.count
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var text: String = ""
|
||||
text += "\(views) views"
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||
arguments.openMessage(message.id)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
||||
// return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: 0, forwards: 0, sectionId: self.section, style: .blocks, action: {
|
||||
// arguments.openMessage(message.id)
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func messageStatsControllerEntries(data: MessageStats?, messages: SearchMessagesResult?, presentationData: PresentationData) -> [StatsEntry] {
|
||||
var entries: [StatsEntry] = []
|
||||
|
||||
if let data = data {
|
||||
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased()))
|
||||
entries.append(.overview(presentationData.theme, data, messages?.totalCount))
|
||||
|
||||
if !data.interactionsGraph.isEmpty {
|
||||
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
|
||||
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
||||
}
|
||||
|
||||
if let messages = messages, !messages.messages.isEmpty {
|
||||
entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased()))
|
||||
var index: Int32 = 0
|
||||
for message in messages.messages {
|
||||
entries.append(.publicForward(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func messageStatsController(context: AccountContext, messageId: MessageId, cachedPeerData: CachedPeerData) -> ViewController {
|
||||
var navigateToMessageImpl: ((MessageId) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
let dataPromise = Promise<MessageStats?>(nil)
|
||||
let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil)
|
||||
|
||||
var datacenterId: Int32 = 0
|
||||
if let cachedData = cachedPeerData as? CachedChannelData {
|
||||
datacenterId = cachedData.statsDatacenterId
|
||||
}
|
||||
|
||||
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: messageId)
|
||||
let dataSignal: Signal<MessageStats?, NoError> = statsContext.state
|
||||
|> map { state in
|
||||
return state.stats
|
||||
}
|
||||
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||
|
||||
let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
|
||||
return statsContext.loadDetailedGraph(graph, x: x)
|
||||
}, openMessage: { messageId in
|
||||
navigateToMessageImpl?(messageId)
|
||||
})
|
||||
|
||||
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
|
||||
|
||||
let previousData = Atomic<MessageStats?>(value: nil)
|
||||
|
||||
let searchSignal = searchMessages(account: context.account, location: .publicForwards(messageId: messageId, datacenterId: Int(datacenterId)), query: "", state: nil)
|
||||
|> map(Optional.init)
|
||||
|> afterNext { result in
|
||||
if let result = result {
|
||||
for message in result.0.messages {
|
||||
if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) {
|
||||
let _ = updatedRemotePeer(postbox: context.account.postbox, network: context.account.network, peer: peerReference).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
messagesPromise.set(.single(nil) |> then(searchSignal))
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, data, search, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let previous = previousData.swap(data)
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if data == nil {
|
||||
if longLoading {
|
||||
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
|
||||
} else {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
}
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Stats_MessageTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, messages: search?.0, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
let _ = statsContext.state
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
controller.contentOffsetChanged = { [weak controller] _, _ in
|
||||
controller?.forEachItemNode({ itemNode in
|
||||
if let itemNode = itemNode as? StatsGraphItemNode {
|
||||
itemNode.resetInteraction()
|
||||
}
|
||||
})
|
||||
}
|
||||
controller.didDisappear = { [weak controller] _ in
|
||||
controller?.clearItemNodesHighlight(animated: true)
|
||||
}
|
||||
navigateToMessageImpl = { [weak controller] messageId in
|
||||
if let navigationController = controller?.navigationController as? NavigationController {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(messageId.peerId), subject: .message(messageId), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
294
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
294
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
|
||||
class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: MessageStats
|
||||
let publicShares: Int32?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(presentationData: ItemListPresentationData, stats: MessageStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.presentationData = presentationData
|
||||
self.stats = stats
|
||||
self.publicShares = publicShares
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = MessageStatsOverviewItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? MessageStatsOverviewItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = false
|
||||
}
|
||||
|
||||
class MessageStatsOverviewItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let leftValueLabel: ImmediateTextNode
|
||||
private let centerValueLabel: ImmediateTextNode
|
||||
private let rightValueLabel: ImmediateTextNode
|
||||
|
||||
private let leftTitleLabel: ImmediateTextNode
|
||||
private let centerTitleLabel: ImmediateTextNode
|
||||
private let rightTitleLabel: ImmediateTextNode
|
||||
|
||||
private var item: MessageStatsOverviewItem?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.leftValueLabel = ImmediateTextNode()
|
||||
self.centerValueLabel = ImmediateTextNode()
|
||||
self.rightValueLabel = ImmediateTextNode()
|
||||
|
||||
self.leftTitleLabel = ImmediateTextNode()
|
||||
self.centerTitleLabel = ImmediateTextNode()
|
||||
self.rightTitleLabel = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.leftValueLabel)
|
||||
self.addSubnode(self.centerValueLabel)
|
||||
self.addSubnode(self.rightValueLabel)
|
||||
|
||||
self.addSubnode(self.leftTitleLabel)
|
||||
self.addSubnode(self.centerTitleLabel)
|
||||
self.addSubnode(self.rightTitleLabel)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel)
|
||||
let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel)
|
||||
let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel)
|
||||
|
||||
let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel)
|
||||
let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel)
|
||||
let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let topInset: CGFloat = 14.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var height: CGFloat = topInset * 2.0
|
||||
|
||||
let leftInset = params.leftInset
|
||||
let rightInset = params.rightInset
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(item.stats.forwards - Int($0)))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Views", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Public Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Private Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
height += rightValueLabelLayoutAndApply!.0.size.height + rightTitleLabelLayoutAndApply!.0.size.height
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: height)
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = leftValueLabelLayoutAndApply?.1()
|
||||
let _ = centerValueLabelLayoutAndApply?.1()
|
||||
let _ = rightValueLabelLayoutAndApply?.1()
|
||||
let _ = leftTitleLabelLayoutAndApply?.1()
|
||||
let _ = centerTitleLabelLayoutAndApply?.1()
|
||||
let _ = rightTitleLabelLayoutAndApply?.1()
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let maxLeftWidth = max(leftValueLabelLayoutAndApply?.0.size.width ?? 0.0, leftTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||
let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||
let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||
|
||||
let horizontalSpacing = min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0)
|
||||
|
||||
var x: CGFloat = leftInset + (params.width - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0
|
||||
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size)
|
||||
strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size)
|
||||
x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing
|
||||
}
|
||||
|
||||
if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size)
|
||||
strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size)
|
||||
x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing
|
||||
}
|
||||
|
||||
if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size)
|
||||
strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
|||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
let leftInset = 16.0 + params.leftInset
|
||||
let rightInset = 16.0 + params.rightInset
|
||||
var totalLeftInset = leftInset
|
||||
let additionalRightInset: CGFloat = 93.0
|
||||
let additionalRightInset: CGFloat = 128.0
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
|
||||
|
|
@ -229,9 +229,9 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (viewsLayout, viewsApply) = makeViewsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageViews(item.views), font: labelFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (viewsLayout, viewsApply) = makeViewsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageViews(item.views), font: labelFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageForwards(item.forwards), font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageForwards(item.forwards), font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let verticalInset: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 3.0
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
|||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
|
||||
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct)
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode?.visibility = true
|
||||
self.animationNode?.addSubnode(self.textNode)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ public final class CachedUserData: CachedPeerData {
|
|||
public let pinnedMessageId: MessageId?
|
||||
public let isBlocked: Bool
|
||||
public let commonGroupCount: Int32
|
||||
public let callsAvailable: Bool
|
||||
public let voiceCallsAvailable: Bool
|
||||
public let videoCallsAvailable: Bool
|
||||
public let callsPrivate: Bool
|
||||
public let canPinMessages: Bool
|
||||
public let hasScheduledMessages: Bool
|
||||
|
|
@ -24,21 +25,23 @@ public final class CachedUserData: CachedPeerData {
|
|||
self.pinnedMessageId = nil
|
||||
self.isBlocked = false
|
||||
self.commonGroupCount = 0
|
||||
self.callsAvailable = false
|
||||
self.voiceCallsAvailable = false
|
||||
self.videoCallsAvailable = false
|
||||
self.callsPrivate = false
|
||||
self.canPinMessages = false
|
||||
self.hasScheduledMessages = false
|
||||
self.messageIds = Set()
|
||||
}
|
||||
|
||||
public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, callsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool) {
|
||||
public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool) {
|
||||
self.about = about
|
||||
self.botInfo = botInfo
|
||||
self.peerStatusSettings = peerStatusSettings
|
||||
self.pinnedMessageId = pinnedMessageId
|
||||
self.isBlocked = isBlocked
|
||||
self.commonGroupCount = commonGroupCount
|
||||
self.callsAvailable = callsAvailable
|
||||
self.voiceCallsAvailable = voiceCallsAvailable
|
||||
self.videoCallsAvailable = videoCallsAvailable
|
||||
self.callsPrivate = callsPrivate
|
||||
self.canPinMessages = canPinMessages
|
||||
self.hasScheduledMessages = hasScheduledMessages
|
||||
|
|
@ -66,7 +69,8 @@ public final class CachedUserData: CachedPeerData {
|
|||
}
|
||||
self.isBlocked = decoder.decodeInt32ForKey("b", orElse: 0) != 0
|
||||
self.commonGroupCount = decoder.decodeInt32ForKey("cg", orElse: 0)
|
||||
self.callsAvailable = decoder.decodeInt32ForKey("ca", orElse: 0) != 0
|
||||
self.voiceCallsAvailable = decoder.decodeInt32ForKey("ca", orElse: 0) != 0
|
||||
self.videoCallsAvailable = decoder.decodeInt32ForKey("vca", orElse: 0) != 0
|
||||
self.callsPrivate = decoder.decodeInt32ForKey("cp", orElse: 0) != 0
|
||||
self.canPinMessages = decoder.decodeInt32ForKey("cpm", orElse: 0) != 0
|
||||
self.hasScheduledMessages = decoder.decodeBoolForKey("hsm", orElse: false)
|
||||
|
|
@ -105,7 +109,8 @@ public final class CachedUserData: CachedPeerData {
|
|||
}
|
||||
encoder.encodeInt32(self.isBlocked ? 1 : 0, forKey: "b")
|
||||
encoder.encodeInt32(self.commonGroupCount, forKey: "cg")
|
||||
encoder.encodeInt32(self.callsAvailable ? 1 : 0, forKey: "ca")
|
||||
encoder.encodeInt32(self.voiceCallsAvailable ? 1 : 0, forKey: "ca")
|
||||
encoder.encodeInt32(self.videoCallsAvailable ? 1 : 0, forKey: "vca")
|
||||
encoder.encodeInt32(self.callsPrivate ? 1 : 0, forKey: "cp")
|
||||
encoder.encodeInt32(self.canPinMessages ? 1 : 0, forKey: "cpm")
|
||||
encoder.encodeBool(self.hasScheduledMessages, forKey: "hsm")
|
||||
|
|
@ -123,46 +128,50 @@ public final class CachedUserData: CachedPeerData {
|
|||
return false
|
||||
}
|
||||
|
||||
return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.callsAvailable == other.callsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages
|
||||
return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages
|
||||
}
|
||||
|
||||
public func withUpdatedAbout(_ about: String?) -> CachedUserData {
|
||||
return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedCallsAvailable(_ callsAvailable: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages)
|
||||
}
|
||||
|
||||
public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData {
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages)
|
||||
return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
|
||||
public class ForwardCountMessageAttribute: MessageAttribute {
|
||||
public let count: Int
|
||||
|
||||
public var associatedMessageIds: [MessageId] = []
|
||||
|
||||
public init(count: Int) {
|
||||
self.count = count
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.count = Int(decoder.decodeInt32ForKey("c", orElse: 0))
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(Int32(self.count), forKey: "c")
|
||||
}
|
||||
}
|
||||
|
|
@ -303,21 +303,25 @@ public final class TelegramMediaImage: Media, Equatable, Codable {
|
|||
public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, CustomStringConvertible {
|
||||
public let dimensions: PixelDimensions
|
||||
public let resource: TelegramMediaResource
|
||||
public let progressiveSizes: [Int32]
|
||||
|
||||
public init(dimensions: PixelDimensions, resource: TelegramMediaResource) {
|
||||
public init(dimensions: PixelDimensions, resource: TelegramMediaResource, progressiveSizes: [Int32]) {
|
||||
self.dimensions = dimensions
|
||||
self.resource = resource
|
||||
self.progressiveSizes = progressiveSizes
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.dimensions = PixelDimensions(width: decoder.decodeInt32ForKey("dx", orElse: 0), height: decoder.decodeInt32ForKey("dy", orElse: 0))
|
||||
self.resource = decoder.decodeObjectForKey("r") as? TelegramMediaResource ?? EmptyMediaResource()
|
||||
self.progressiveSizes = decoder.decodeInt32ArrayForKey("ps")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.dimensions.width, forKey: "dx")
|
||||
encoder.encodeInt32(self.dimensions.height, forKey: "dy")
|
||||
encoder.encodeObject(self.resource, forKey: "r")
|
||||
encoder.encodeInt32Array(self.progressiveSizes, forKey: "ps")
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
|
|
@ -331,6 +335,9 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C
|
|||
if !self.resource.id.isEqual(to: other.resource.id) {
|
||||
return false
|
||||
}
|
||||
if self.progressiveSizes != other.progressiveSizes {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
|
||||
|
||||
public class ViewCountMessageAttribute: MessageAttribute {
|
||||
public let count: Int
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) }
|
||||
dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) }
|
||||
dict[643940105] = { return Api.Update.parse_updatePhoneCallSignalingData($0) }
|
||||
dict[1708307556] = { return Api.Update.parse_updateChannelParticipant($0) }
|
||||
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
|
||||
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
|
||||
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
||||
|
|
@ -290,6 +291,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[2009052699] = { return Api.PhotoSize.parse_photoSize($0) }
|
||||
dict[-374917894] = { return Api.PhotoSize.parse_photoCachedSize($0) }
|
||||
dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) }
|
||||
dict[1520986705] = { return Api.PhotoSize.parse_photoSizeProgressive($0) }
|
||||
dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) }
|
||||
dict[-463889475] = { return Api.messages.Stickers.parse_stickers($0) }
|
||||
dict[-1096616924] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }
|
||||
|
|
@ -703,6 +705,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-395967805] = { return Api.messages.AllStickers.parse_allStickersNotModified($0) }
|
||||
dict[-302170017] = { return Api.messages.AllStickers.parse_allStickers($0) }
|
||||
dict[-1655957568] = { return Api.PhoneConnection.parse_phoneConnection($0) }
|
||||
dict[1667228533] = { return Api.PhoneConnection.parse_phoneConnectionWebrtc($0) }
|
||||
dict[-206688531] = { return Api.help.UserInfo.parse_userInfoEmpty($0) }
|
||||
dict[32192344] = { return Api.help.UserInfo.parse_userInfo($0) }
|
||||
dict[-1194283041] = { return Api.AccountDaysTTL.parse_accountDaysTTL($0) }
|
||||
|
|
|
|||
|
|
@ -6037,6 +6037,7 @@ public extension Api {
|
|||
case updateDialogFilterOrder(order: [Int32])
|
||||
case updateDialogFilters
|
||||
case updatePhoneCallSignalingData(phoneCallId: Int64, data: Buffer)
|
||||
case updateChannelParticipant(flags: Int32, channelId: Int32, date: Int32, userId: Int32, prevParticipant: Api.ChannelParticipant?, newParticipant: Api.ChannelParticipant?, qts: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
|
|
@ -6717,6 +6718,18 @@ public extension Api {
|
|||
serializeInt64(phoneCallId, buffer: buffer, boxed: false)
|
||||
serializeBytes(data, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateChannelParticipant(let flags, let channelId, let date, let userId, let prevParticipant, let newParticipant, let qts):
|
||||
if boxed {
|
||||
buffer.appendInt32(1708307556)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(channelId, buffer: buffer, boxed: false)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
serializeInt32(userId, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {prevParticipant!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {newParticipant!.serialize(buffer, true)}
|
||||
serializeInt32(qts, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6884,6 +6897,8 @@ public extension Api {
|
|||
return ("updateDialogFilters", [])
|
||||
case .updatePhoneCallSignalingData(let phoneCallId, let data):
|
||||
return ("updatePhoneCallSignalingData", [("phoneCallId", phoneCallId), ("data", data)])
|
||||
case .updateChannelParticipant(let flags, let channelId, let date, let userId, let prevParticipant, let newParticipant, let qts):
|
||||
return ("updateChannelParticipant", [("flags", flags), ("channelId", channelId), ("date", date), ("userId", userId), ("prevParticipant", prevParticipant), ("newParticipant", newParticipant), ("qts", qts)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8229,6 +8244,39 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateChannelParticipant(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: Api.ChannelParticipant?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant
|
||||
} }
|
||||
var _6: Api.ChannelParticipant?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
|
||||
_6 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant
|
||||
} }
|
||||
var _7: Int32?
|
||||
_7 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
|
||||
let _c7 = _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.Update.updateChannelParticipant(flags: _1!, channelId: _2!, date: _3!, userId: _4!, prevParticipant: _5, newParticipant: _6, qts: _7!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum PopularContact: TypeConstructorDescription {
|
||||
|
|
@ -9090,6 +9138,7 @@ public extension Api {
|
|||
case photoSize(type: String, location: Api.FileLocation, w: Int32, h: Int32, size: Int32)
|
||||
case photoCachedSize(type: String, location: Api.FileLocation, w: Int32, h: Int32, bytes: Buffer)
|
||||
case photoStrippedSize(type: String, bytes: Buffer)
|
||||
case photoSizeProgressive(type: String, location: Api.FileLocation, w: Int32, h: Int32, sizes: [Int32])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
|
|
@ -9126,6 +9175,20 @@ public extension Api {
|
|||
serializeString(type, buffer: buffer, boxed: false)
|
||||
serializeBytes(bytes, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .photoSizeProgressive(let type, let location, let w, let h, let sizes):
|
||||
if boxed {
|
||||
buffer.appendInt32(1520986705)
|
||||
}
|
||||
serializeString(type, buffer: buffer, boxed: false)
|
||||
location.serialize(buffer, true)
|
||||
serializeInt32(w, buffer: buffer, boxed: false)
|
||||
serializeInt32(h, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(sizes.count))
|
||||
for item in sizes {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9139,6 +9202,8 @@ public extension Api {
|
|||
return ("photoCachedSize", [("type", type), ("location", location), ("w", w), ("h", h), ("bytes", bytes)])
|
||||
case .photoStrippedSize(let type, let bytes):
|
||||
return ("photoStrippedSize", [("type", type), ("bytes", bytes)])
|
||||
case .photoSizeProgressive(let type, let location, let w, let h, let sizes):
|
||||
return ("photoSizeProgressive", [("type", type), ("location", location), ("w", w), ("h", h), ("sizes", sizes)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9217,6 +9282,33 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_photoSizeProgressive(_ reader: BufferReader) -> PhotoSize? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
var _2: Api.FileLocation?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.FileLocation
|
||||
}
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: [Int32]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.PhotoSize.photoSizeProgressive(type: _1!, location: _2!, w: _3!, h: _4!, sizes: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum GlobalPrivacySettings: TypeConstructorDescription {
|
||||
|
|
@ -19749,6 +19841,7 @@ public extension Api {
|
|||
}
|
||||
public enum PhoneConnection: TypeConstructorDescription {
|
||||
case phoneConnection(id: Int64, ip: String, ipv6: String, port: Int32, peerTag: Buffer)
|
||||
case phoneConnectionWebrtc(flags: Int32, id: Int64, ip: String, ipv6: String, port: Int32, username: String, password: String)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
|
|
@ -19762,6 +19855,18 @@ public extension Api {
|
|||
serializeInt32(port, buffer: buffer, boxed: false)
|
||||
serializeBytes(peerTag, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .phoneConnectionWebrtc(let flags, let id, let ip, let ipv6, let port, let username, let password):
|
||||
if boxed {
|
||||
buffer.appendInt32(1667228533)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
serializeString(ip, buffer: buffer, boxed: false)
|
||||
serializeString(ipv6, buffer: buffer, boxed: false)
|
||||
serializeInt32(port, buffer: buffer, boxed: false)
|
||||
serializeString(username, buffer: buffer, boxed: false)
|
||||
serializeString(password, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19769,6 +19874,8 @@ public extension Api {
|
|||
switch self {
|
||||
case .phoneConnection(let id, let ip, let ipv6, let port, let peerTag):
|
||||
return ("phoneConnection", [("id", id), ("ip", ip), ("ipv6", ipv6), ("port", port), ("peerTag", peerTag)])
|
||||
case .phoneConnectionWebrtc(let flags, let id, let ip, let ipv6, let port, let username, let password):
|
||||
return ("phoneConnectionWebrtc", [("flags", flags), ("id", id), ("ip", ip), ("ipv6", ipv6), ("port", port), ("username", username), ("password", password)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19795,6 +19902,35 @@ public extension Api {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_phoneConnectionWebrtc(_ reader: BufferReader) -> PhoneConnection? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: String?
|
||||
_4 = parseString(reader)
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
var _6: String?
|
||||
_6 = parseString(reader)
|
||||
var _7: String?
|
||||
_7 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
let _c7 = _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.PhoneConnection.phoneConnectionWebrtc(flags: _1!, id: _2!, ip: _3!, ipv6: _4!, port: _5!, username: _6!, password: _7!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum AccountDaysTTL: TypeConstructorDescription {
|
||||
|
|
|
|||
|
|
@ -5369,6 +5369,34 @@ public extension Api {
|
|||
})
|
||||
}
|
||||
|
||||
public static func getPromoData() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.help.PromoData>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1063816159)
|
||||
|
||||
return (FunctionDescription(name: "help.getPromoData", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.PromoData? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.help.PromoData?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.help.PromoData
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func hidePromoData(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(505748629)
|
||||
peer.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "help.hidePromoData", parameters: [("peer", peer)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func editUserInfo(userId: Api.InputUser, message: String, entities: [Api.MessageEntity]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.help.UserInfo>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1723407216)
|
||||
|
|
@ -5403,34 +5431,6 @@ public extension Api {
|
|||
})
|
||||
}
|
||||
|
||||
public static func getPromoData() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.help.PromoData>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1063816159)
|
||||
|
||||
return (FunctionDescription(name: "help.getPromoData", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.PromoData? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.help.PromoData?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.help.PromoData
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func hidePromoData(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(505748629)
|
||||
peer.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "help.hidePromoData", parameters: [("peer", peer)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func dismissSuggestion(suggestion: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(125807007)
|
||||
|
|
@ -6802,14 +6802,11 @@ public extension Api {
|
|||
})
|
||||
}
|
||||
|
||||
public static func uploadProfilePhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.photos.Photo>) {
|
||||
public static func updateProfilePhoto(id: Api.InputPhoto) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.photos.Photo>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1980559511)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "photos.uploadProfilePhoto", parameters: [("flags", flags), ("file", file), ("video", video), ("videoStartTs", videoStartTs)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in
|
||||
buffer.appendInt32(1926525996)
|
||||
id.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "photos.updateProfilePhoto", parameters: [("id", id)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.photos.Photo?
|
||||
if let signature = reader.readInt32() {
|
||||
|
|
@ -6819,11 +6816,14 @@ public extension Api {
|
|||
})
|
||||
}
|
||||
|
||||
public static func updateProfilePhoto(id: Api.InputPhoto) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.photos.Photo>) {
|
||||
public static func uploadProfilePhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.photos.Photo>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1926525996)
|
||||
id.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "photos.updateProfilePhoto", parameters: [("id", id)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in
|
||||
buffer.appendInt32(-1980559511)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "photos.uploadProfilePhoto", parameters: [("flags", flags), ("file", file), ("video", video), ("videoStartTs", videoStartTs)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.photos.Photo?
|
||||
if let signature = reader.readInt32() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ static_library(
|
|||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
|
|
@ -21,6 +21,7 @@ static_library(
|
|||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramCallsUI/CallsEmoji:CallsEmoji",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ swift_library(
|
|||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramCallsUI/CallsEmoji:CallsEmoji",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Display
|
|||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import SemanticStatusNode
|
||||
|
||||
private let labelFont = Font.regular(13.0)
|
||||
|
||||
|
|
@ -32,18 +33,22 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
var appearance: Appearance
|
||||
var image: Image
|
||||
var isEnabled: Bool
|
||||
var hasProgress: Bool
|
||||
|
||||
init(appearance: Appearance, image: Image, isEnabled: Bool = true) {
|
||||
init(appearance: Appearance, image: Image, isEnabled: Bool = true, hasProgress: Bool = false) {
|
||||
self.appearance = appearance
|
||||
self.image = image
|
||||
self.isEnabled = isEnabled
|
||||
self.hasProgress = hasProgress
|
||||
}
|
||||
}
|
||||
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let effectView: UIVisualEffectView
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
private let contentNode: ASImageNode
|
||||
private let overlayHighlightNode: ASImageNode
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private let largeButtonSize: CGFloat = 72.0
|
||||
|
|
@ -60,6 +65,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
self.effectView.clipsToBounds = true
|
||||
self.effectView.isUserInteractionEnabled = false
|
||||
|
||||
self.contentBackgroundNode = ASImageNode()
|
||||
self.contentBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.contentNode = ASImageNode()
|
||||
self.contentNode.isUserInteractionEnabled = false
|
||||
|
||||
|
|
@ -79,6 +87,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
self.addSubnode(self.textNode)
|
||||
|
||||
self.contentContainer.view.addSubview(self.effectView)
|
||||
self.contentContainer.addSubnode(self.contentBackgroundNode)
|
||||
self.contentContainer.addSubnode(self.contentNode)
|
||||
self.contentContainer.addSubnode(self.overlayHighlightNode)
|
||||
|
||||
|
|
@ -88,9 +97,13 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
}
|
||||
if highlighted {
|
||||
strongSelf.overlayHighlightNode.alpha = 1.0
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
||||
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.9)
|
||||
} else {
|
||||
strongSelf.overlayHighlightNode.alpha = 0.0
|
||||
strongSelf.overlayHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
||||
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -101,12 +114,34 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
let isSmall = self.largeButtonSize > size.width
|
||||
|
||||
self.effectView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.contentBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
|
||||
if self.currentContent != content {
|
||||
self.currentContent = content
|
||||
|
||||
if content.hasProgress {
|
||||
if self.statusNode == nil {
|
||||
let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear)
|
||||
self.statusNode = statusNode
|
||||
self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode)
|
||||
statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0)), animated: false, completion: {})
|
||||
}
|
||||
if let statusNode = self.statusNode {
|
||||
statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
if transition.isAnimated {
|
||||
statusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else if let statusNode = self.statusNode {
|
||||
self.statusNode = nil
|
||||
transition.updateAlpha(node: statusNode, alpha: 0.0, completion: { [weak statusNode] _ in
|
||||
statusNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
switch content.appearance {
|
||||
case .blurred:
|
||||
self.effectView.isHidden = false
|
||||
|
|
@ -117,19 +152,29 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
self.alpha = content.isEnabled ? 1.0 : 0.7
|
||||
self.isUserInteractionEnabled = content.isEnabled
|
||||
|
||||
let contentBackgroundImage: UIImage? = nil
|
||||
|
||||
let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
var fillColor: UIColor = .clear
|
||||
var imageColor: UIColor = .white
|
||||
var drawOverMask = false
|
||||
context.setBlendMode(.normal)
|
||||
var imageScale: CGFloat = 1.0
|
||||
switch content.appearance {
|
||||
case let .blurred(isFilled):
|
||||
if isFilled {
|
||||
fillColor = .white
|
||||
drawOverMask = true
|
||||
if content.hasProgress {
|
||||
fillColor = .clear
|
||||
imageColor = .black
|
||||
drawOverMask = false
|
||||
context.setBlendMode(.copy)
|
||||
} else {
|
||||
if isFilled {
|
||||
fillColor = .white
|
||||
drawOverMask = true
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
}
|
||||
let smallButtonSize: CGFloat = 60.0
|
||||
imageScale = self.largeButtonSize / smallButtonSize
|
||||
|
|
@ -149,19 +194,19 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
|
||||
switch content.image {
|
||||
case .camera:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCameraButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCameraButton"), color: imageColor)
|
||||
case .mute:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), color: imageColor)
|
||||
case .flipCamera:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: imageColor)
|
||||
case .bluetooth:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallBluetoothButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallBluetoothButton"), color: imageColor)
|
||||
case .speaker:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), color: imageColor)
|
||||
case .accept:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAcceptButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAcceptButton"), color: imageColor)
|
||||
case .end:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallDeclineButton"), color: .white)
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallDeclineButton"), color: imageColor)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
|
|
@ -180,6 +225,14 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
if transition.isAnimated, let contentBackgroundImage = contentBackgroundImage, let previousContent = self.contentBackgroundNode.image {
|
||||
self.contentBackgroundNode.image = contentBackgroundImage
|
||||
self.contentBackgroundNode.layer.animate(from: previousContent.cgImage!, to: contentBackgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
||||
} else {
|
||||
self.contentBackgroundNode.image = contentBackgroundImage
|
||||
}
|
||||
|
||||
if transition.isAnimated, let contentImage = contentImage, let previousContent = self.contentNode.image {
|
||||
self.contentNode.image = contentImage
|
||||
self.contentNode.layer.animate(from: previousContent.cgImage!, to: contentImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ enum CallControllerButtonsSpeakerMode {
|
|||
enum CallControllerButtonsMode: Equatable {
|
||||
enum VideoState: Equatable {
|
||||
case notAvailable
|
||||
case possible(Bool)
|
||||
case outgoingRequested
|
||||
case incomingRequested
|
||||
case possible(isEnabled: Bool, isInitializing: Bool)
|
||||
case outgoingRequested(isInitializing: Bool)
|
||||
case incomingRequested(sendsVideo: Bool)
|
||||
case active
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ private enum ButtonDescription: Equatable {
|
|||
|
||||
case accept
|
||||
case end(EndType)
|
||||
case enableCamera(Bool, Bool)
|
||||
case enableCamera(Bool, Bool, Bool)
|
||||
case switchCamera
|
||||
case soundOutput(SoundOutput)
|
||||
case mute(Bool)
|
||||
|
|
@ -110,6 +110,10 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
|
||||
private var appliedMode: CallControllerButtonsMode?
|
||||
|
||||
func videoButtonFrame() -> CGRect? {
|
||||
return self.buttonNodes[.enableCamera]?.frame
|
||||
}
|
||||
|
||||
private func updateButtonsLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, width: CGFloat, bottomInset: CGFloat, animated: Bool) -> CGFloat {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
|
|
@ -171,12 +175,12 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
mappedState = .outgoingRinging
|
||||
case let .active(_, videoStateValue):
|
||||
switch videoStateValue {
|
||||
case .incomingRequested:
|
||||
mappedState = .incomingRinging
|
||||
videoState = .outgoingRequested
|
||||
case .outgoingRequested:
|
||||
mappedState = .outgoingRinging
|
||||
videoState = .outgoingRequested
|
||||
case let .incomingRequested(sendsVideo):
|
||||
mappedState = .active
|
||||
videoState = .incomingRequested(sendsVideo: sendsVideo)
|
||||
case let .outgoingRequested(isInitializing):
|
||||
mappedState = .active
|
||||
videoState = .outgoingRequested(isInitializing: isInitializing)
|
||||
case .active, .possible, .notAvailable:
|
||||
mappedState = .active
|
||||
}
|
||||
|
|
@ -204,14 +208,17 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
case .active, .possible, .incomingRequested, .outgoingRequested:
|
||||
let isCameraActive: Bool
|
||||
let isCameraEnabled: Bool
|
||||
if case let .possible(value) = videoState {
|
||||
let isCameraInitializing: Bool
|
||||
if case let .possible(value, isInitializing) = videoState {
|
||||
isCameraActive = false
|
||||
isCameraEnabled = value
|
||||
isCameraInitializing = isInitializing
|
||||
} else {
|
||||
isCameraActive = !self.isCameraPaused
|
||||
isCameraEnabled = true
|
||||
isCameraInitializing = false
|
||||
}
|
||||
topButtons.append(.enableCamera(isCameraActive, isCameraEnabled))
|
||||
topButtons.append(.enableCamera(isCameraActive, isCameraEnabled, isCameraInitializing))
|
||||
topButtons.append(.mute(self.isMuted))
|
||||
if case .possible = videoState {
|
||||
topButtons.append(.soundOutput(soundOutput))
|
||||
|
|
@ -256,12 +263,19 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
case .active, .incomingRequested, .outgoingRequested:
|
||||
let isCameraActive: Bool
|
||||
let isCameraEnabled: Bool
|
||||
if case let .possible(value) = videoState {
|
||||
var isCameraInitializing: Bool
|
||||
if case .incomingRequested = videoState {
|
||||
isCameraActive = false
|
||||
isCameraEnabled = true
|
||||
isCameraInitializing = false
|
||||
} else if case let .possible(value, isInitializing) = videoState {
|
||||
isCameraActive = false
|
||||
isCameraEnabled = value
|
||||
isCameraInitializing = isInitializing
|
||||
} else {
|
||||
isCameraActive = !self.isCameraPaused
|
||||
isCameraEnabled = true
|
||||
isCameraInitializing = false
|
||||
}
|
||||
|
||||
var topButtons: [ButtonDescription] = []
|
||||
|
|
@ -278,7 +292,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
soundOutput = .bluetooth
|
||||
}
|
||||
|
||||
topButtons.append(.enableCamera(isCameraActive, isCameraEnabled))
|
||||
if case let .outgoingRequested(isInitializing) = videoState {
|
||||
isCameraInitializing = isInitializing
|
||||
}
|
||||
|
||||
topButtons.append(.enableCamera(isCameraActive, isCameraEnabled, isCameraInitializing))
|
||||
topButtons.append(.mute(isMuted))
|
||||
topButtons.append(.switchCamera)
|
||||
topButtons.append(.end(.end))
|
||||
|
|
@ -298,6 +316,19 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
var topButtons: [ButtonDescription] = []
|
||||
var bottomButtons: [ButtonDescription] = []
|
||||
|
||||
let isCameraActive: Bool
|
||||
let isCameraEnabled: Bool
|
||||
var isCameraInitializing: Bool
|
||||
if case let .possible(value, isInitializing) = videoState {
|
||||
isCameraActive = false
|
||||
isCameraEnabled = value
|
||||
isCameraInitializing = isInitializing
|
||||
} else {
|
||||
isCameraActive = false
|
||||
isCameraEnabled = true
|
||||
isCameraInitializing = false
|
||||
}
|
||||
|
||||
let soundOutput: ButtonDescription.SoundOutput
|
||||
switch speakerMode {
|
||||
case .none, .builtin:
|
||||
|
|
@ -310,7 +341,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
soundOutput = .bluetooth
|
||||
}
|
||||
|
||||
topButtons.append(.enableCamera(false, true))
|
||||
topButtons.append(.enableCamera(isCameraActive, isCameraEnabled, isCameraInitializing))
|
||||
topButtons.append(.mute(self.isMuted))
|
||||
topButtons.append(.soundOutput(soundOutput))
|
||||
|
||||
|
|
@ -379,11 +410,12 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
case .end:
|
||||
buttonText = strings.Call_End
|
||||
}
|
||||
case let .enableCamera(isActivated, isEnabled):
|
||||
case let .enableCamera(isActivated, isEnabled, isInitializing):
|
||||
buttonContent = CallControllerButtonItemNode.Content(
|
||||
appearance: .blurred(isFilled: isActivated),
|
||||
image: .camera,
|
||||
isEnabled: isEnabled
|
||||
isEnabled: isEnabled,
|
||||
hasProgress: isInitializing
|
||||
)
|
||||
buttonText = strings.Call_Camera
|
||||
case .switchCamera:
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ private final class CallVideoNode: ASDisplayNode {
|
|||
private(set) var isReady: Bool = false
|
||||
private var isReadyTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private let isFlippedUpdated: () -> Void
|
||||
private let isFlippedUpdated: (CallVideoNode) -> Void
|
||||
|
||||
private(set) var currentOrientation: PresentationCallVideoView.Orientation
|
||||
|
||||
init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping () -> Void) {
|
||||
init(videoView: PresentationCallVideoView, assumeReadyAfterTimeout: Bool, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping (CallVideoNode) -> Void) {
|
||||
self.isReadyUpdated = isReadyUpdated
|
||||
self.isFlippedUpdated = isFlippedUpdated
|
||||
|
||||
|
|
@ -46,46 +46,60 @@ private final class CallVideoNode: ASDisplayNode {
|
|||
self.videoTransformContainer.clipsToBounds = true
|
||||
self.videoView = videoView
|
||||
videoView.view.clipsToBounds = true
|
||||
videoView.view.backgroundColor = .black
|
||||
|
||||
self.currentOrientation = videoView.getOrientation()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.videoTransformContainer.view.addSubview(self.videoView.view)
|
||||
self.addSubnode(self.videoTransformContainer)
|
||||
|
||||
self.videoView.setOnFirstFrameReceived { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyTimer?.invalidate()
|
||||
strongSelf.isReadyUpdated()
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyTimer?.invalidate()
|
||||
strongSelf.isReadyUpdated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.videoView.setOnOrientationUpdated { [weak self] orientation in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.currentOrientation != orientation {
|
||||
strongSelf.currentOrientation = orientation
|
||||
orientationUpdated()
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.currentOrientation != orientation {
|
||||
strongSelf.currentOrientation = orientation
|
||||
orientationUpdated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
self.videoView.setOnIsMirroredUpdated { [weak self] _ in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isFlippedUpdated(strongSelf)
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyUpdated()
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
}
|
||||
|
||||
if assumeReadyAfterTimeout {
|
||||
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyUpdated()
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
}
|
||||
self.isReadyTimer?.start()
|
||||
}
|
||||
|
||||
|
|
@ -93,8 +107,43 @@ private final class CallVideoNode: ASDisplayNode {
|
|||
self.isReadyTimer?.invalidate()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
func animateRadialMask(from fromRect: CGRect, to toRect: CGRect) {
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.frame = fromRect
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addEllipse(in: CGRect(origin: CGPoint(), size: fromRect.size))
|
||||
maskLayer.path = path
|
||||
|
||||
self.layer.mask = maskLayer
|
||||
|
||||
let topLeft = CGPoint(x: 0.0, y: 0.0)
|
||||
let topRight = CGPoint(x: self.bounds.width, y: 0.0)
|
||||
let bottomLeft = CGPoint(x: 0.0, y: self.bounds.height)
|
||||
let bottomRight = CGPoint(x: self.bounds.width, y: self.bounds.height)
|
||||
|
||||
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
|
||||
let dx = v1.x - v2.x
|
||||
let dy = v1.y - v2.y
|
||||
return sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
var maxRadius = distance(toRect.center, topLeft)
|
||||
maxRadius = max(maxRadius, distance(toRect.center, topRight))
|
||||
maxRadius = max(maxRadius, distance(toRect.center, bottomLeft))
|
||||
maxRadius = max(maxRadius, distance(toRect.center, bottomRight))
|
||||
maxRadius = ceil(maxRadius)
|
||||
|
||||
let targetFrame = CGRect(origin: CGPoint(x: toRect.center.x - maxRadius, y: toRect.center.y - maxRadius), size: CGSize(width: maxRadius * 2.0, height: maxRadius * 2.0))
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
transition.updatePosition(layer: maskLayer, position: targetFrame.center)
|
||||
transition.updateTransformScale(layer: maskLayer, scale: maxRadius * 2.0 / fromRect.width, completion: { [weak self] _ in
|
||||
self?.layer.mask = nil
|
||||
})
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.currentCornerRadius = cornerRadius
|
||||
|
||||
var rotationAngle: CGFloat
|
||||
|
|
@ -227,9 +276,13 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
|
||||
private var incomingVideoNodeValue: CallVideoNode?
|
||||
private var incomingVideoViewRequested: Bool = false
|
||||
private var candidateOutgoingVideoNodeValue: CallVideoNode?
|
||||
private var outgoingVideoNodeValue: CallVideoNode?
|
||||
private var outgoingVideoViewRequested: Bool = false
|
||||
|
||||
private var isRequestingVideo: Bool = false
|
||||
private var animateRequestedVideoOnce: Bool = false
|
||||
|
||||
private var expandedVideoNode: CallVideoNode?
|
||||
private var minimizedVideoNode: CallVideoNode?
|
||||
private var disableAnimationForExpandedVideoOnce: Bool = false
|
||||
|
|
@ -396,7 +449,19 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
switch callState.state {
|
||||
case .active:
|
||||
if strongSelf.outgoingVideoNodeValue == nil {
|
||||
strongSelf.call.requestVideo()
|
||||
switch callState.videoState {
|
||||
case .possible:
|
||||
strongSelf.isRequestingVideo = true
|
||||
strongSelf.updateButtonsMode()
|
||||
default:
|
||||
break
|
||||
}
|
||||
switch callState.videoState {
|
||||
case .incomingRequested:
|
||||
strongSelf.call.acceptVideo()
|
||||
default:
|
||||
strongSelf.call.requestVideo()
|
||||
}
|
||||
} else {
|
||||
strongSelf.isVideoPaused = !strongSelf.isVideoPaused
|
||||
strongSelf.outgoingVideoNodeValue?.updateIsBlurred(isBlurred: strongSelf.isVideoPaused)
|
||||
|
|
@ -417,7 +482,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
return
|
||||
}
|
||||
strongSelf.call.switchVideoCamera()
|
||||
if let outgoingVideoNode = strongSelf.outgoingVideoNodeValue {
|
||||
if let _ = strongSelf.outgoingVideoNodeValue {
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
|
|
@ -487,7 +552,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
|
||||
private func setupAudioOutputs() {
|
||||
if self.outgoingVideoNodeValue != nil {
|
||||
if self.outgoingVideoNodeValue != nil || self.candidateOutgoingVideoNodeValue != nil {
|
||||
if let audioOutputState = self.audioOutputState, let currentOutput = audioOutputState.currentOutput {
|
||||
switch currentOutput {
|
||||
case .headphones:
|
||||
|
|
@ -508,7 +573,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
var statusReception: Int32?
|
||||
|
||||
switch callState.videoState {
|
||||
case .active:
|
||||
case .active, .incomingRequested(true):
|
||||
if !self.incomingVideoViewRequested {
|
||||
self.incomingVideoViewRequested = true
|
||||
self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in
|
||||
|
|
@ -516,7 +581,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
return
|
||||
}
|
||||
if let incomingVideoView = incomingVideoView {
|
||||
let incomingVideoNode = CallVideoNode(videoView: incomingVideoView, isReadyUpdated: {
|
||||
let incomingVideoNode = CallVideoNode(videoView: incomingVideoView, assumeReadyAfterTimeout: false, isReadyUpdated: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -530,13 +595,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}, isFlippedUpdated: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}, isFlippedUpdated: { _ in
|
||||
})
|
||||
strongSelf.incomingVideoNodeValue = incomingVideoNode
|
||||
strongSelf.expandedVideoNode = incomingVideoNode
|
||||
|
|
@ -552,40 +611,80 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
|
||||
switch callState.videoState {
|
||||
case .active, .outgoingRequested, .incomingRequested:
|
||||
case .active, .outgoingRequested, .incomingRequested(false):
|
||||
if !self.outgoingVideoViewRequested {
|
||||
self.outgoingVideoViewRequested = true
|
||||
let delayUntilInitialized = self.isRequestingVideo
|
||||
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let outgoingVideoView = outgoingVideoView {
|
||||
outgoingVideoView.view.backgroundColor = .black
|
||||
outgoingVideoView.view.clipsToBounds = true
|
||||
let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, isReadyUpdated: {}, orientationUpdated: {
|
||||
|
||||
let applyNode: () -> Void = {
|
||||
guard let strongSelf = self, let outgoingVideoNode = strongSelf.candidateOutgoingVideoNodeValue else {
|
||||
return
|
||||
}
|
||||
strongSelf.candidateOutgoingVideoNodeValue = nil
|
||||
|
||||
if strongSelf.isRequestingVideo {
|
||||
strongSelf.isRequestingVideo = false
|
||||
strongSelf.animateRequestedVideoOnce = true
|
||||
}
|
||||
|
||||
strongSelf.outgoingVideoNodeValue = outgoingVideoNode
|
||||
strongSelf.minimizedVideoNode = outgoingVideoNode
|
||||
if let expandedVideoNode = strongSelf.expandedVideoNode {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: expandedVideoNode)
|
||||
} else {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||
}
|
||||
strongSelf.updateButtonsMode(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, assumeReadyAfterTimeout: true, isReadyUpdated: {
|
||||
if delayUntilInitialized {
|
||||
Queue.mainQueue().after(0.4, {
|
||||
applyNode()
|
||||
})
|
||||
}
|
||||
}, orientationUpdated: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}, isFlippedUpdated: {
|
||||
guard let strongSelf = self else {
|
||||
}, isFlippedUpdated: { videoNode in
|
||||
guard let _ = self else {
|
||||
return
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
/*if videoNode === strongSelf.minimizedVideoNode, let tempView = videoNode.view.snapshotView(afterScreenUpdates: true) {
|
||||
videoNode.view.superview?.insertSubview(tempView, aboveSubview: videoNode.view)
|
||||
videoNode.view.frame = videoNode.frame
|
||||
let transitionOptions: UIView.AnimationOptions = [.transitionFlipFromRight, .showHideTransitionViews]
|
||||
|
||||
UIView.transition(with: tempView, duration: 1.0, options: transitionOptions, animations: {
|
||||
tempView.isHidden = true
|
||||
}, completion: { [weak tempView] _ in
|
||||
tempView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
videoNode.view.isHidden = true
|
||||
UIView.transition(with: videoNode.view, duration: 1.0, options: transitionOptions, animations: {
|
||||
videoNode.view.isHidden = false
|
||||
})
|
||||
}*/
|
||||
})
|
||||
strongSelf.outgoingVideoNodeValue = outgoingVideoNode
|
||||
strongSelf.minimizedVideoNode = outgoingVideoNode
|
||||
if let expandedVideoNode = strongSelf.expandedVideoNode {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: expandedVideoNode)
|
||||
} else {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
||||
strongSelf.candidateOutgoingVideoNodeValue = outgoingVideoNode
|
||||
strongSelf.setupAudioOutputs()
|
||||
|
||||
if !delayUntilInitialized {
|
||||
applyNode()
|
||||
}
|
||||
strongSelf.setupAudioOutputs()
|
||||
}
|
||||
|
|
@ -679,7 +778,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
}
|
||||
switch callState.videoState {
|
||||
case .notAvailable, .active, .possible:
|
||||
case .notAvailable, .active, .possible, .outgoingRequested:
|
||||
statusValue = .timer({ value in
|
||||
if isReconnecting {
|
||||
return strings.Call_StatusConnecting
|
||||
|
|
@ -695,8 +794,8 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
text += "\n\(self.statusNode.subtitle)"
|
||||
}
|
||||
statusValue = .text(string: text, displayLogo: true)
|
||||
case .outgoingRequested:
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusRequesting, displayLogo: false)
|
||||
/*case .outgoingRequested:
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusRequesting, displayLogo: false)*/
|
||||
}
|
||||
}
|
||||
if self.shouldStayHiddenUntilConnection {
|
||||
|
|
@ -732,7 +831,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
|
||||
private var buttonsTerminationMode: CallControllerButtonsMode?
|
||||
|
||||
private func updateButtonsMode() {
|
||||
private func updateButtonsMode(transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)) {
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
|
|
@ -765,11 +864,15 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
default:
|
||||
break
|
||||
}
|
||||
mappedVideoState = .possible(isEnabled)
|
||||
mappedVideoState = .possible(isEnabled: isEnabled, isInitializing: false)
|
||||
case .outgoingRequested:
|
||||
mappedVideoState = .outgoingRequested
|
||||
case .incomingRequested:
|
||||
mappedVideoState = .incomingRequested
|
||||
if self.outgoingVideoNodeValue != nil {
|
||||
mappedVideoState = .outgoingRequested(isInitializing: self.isRequestingVideo)
|
||||
} else {
|
||||
mappedVideoState = .possible(isEnabled: true, isInitializing: self.isRequestingVideo)
|
||||
}
|
||||
case let .incomingRequested(sendsVideo):
|
||||
mappedVideoState = .incomingRequested(sendsVideo: sendsVideo)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
}
|
||||
|
|
@ -793,7 +896,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -919,6 +1022,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
|
||||
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
|
||||
|
||||
let previousVideoButtonFrame = self.buttonsNode.videoButtonFrame().flatMap { frame -> CGRect in
|
||||
return self.buttonsNode.view.convert(frame, to: self.view)
|
||||
}
|
||||
|
||||
let buttonsHeight: CGFloat
|
||||
if let buttonsMode = self.buttonsMode {
|
||||
buttonsHeight = self.buttonsNode.updateLayout(strings: self.presentationData.strings, mode: buttonsMode, constrainedWidth: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
|
||||
|
|
@ -995,7 +1102,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
|
||||
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0))
|
||||
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
|
||||
|
||||
|
||||
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
|
||||
transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha)
|
||||
|
||||
|
|
@ -1014,8 +1121,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
if let minimizedVideoNode = self.minimizedVideoNode {
|
||||
var minimizedVideoTransition = transition
|
||||
var didAppear = false
|
||||
if minimizedVideoNode.frame.isEmpty {
|
||||
minimizedVideoTransition = .immediate
|
||||
didAppear = true
|
||||
}
|
||||
if let expandedVideoNode = self.expandedVideoNode, expandedVideoNode.isReady {
|
||||
if self.minimizedVideoDraggingPosition == nil {
|
||||
|
|
@ -1031,10 +1140,23 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
|
|||
}
|
||||
minimizedVideoTransition.updateFrame(node: minimizedVideoNode, frame: previewVideoFrame)
|
||||
minimizedVideoNode.updateLayout(size: minimizedVideoNode.frame.size, cornerRadius: interpolate(from: 14.0, to: 24.0, value: self.pictureInPictureTransitionFraction), transition: minimizedVideoTransition)
|
||||
if transition.isAnimated && didAppear {
|
||||
minimizedVideoNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
minimizedVideoNode.frame = fullscreenVideoFrame
|
||||
minimizedVideoNode.updateLayout(size: layout.size, cornerRadius: 0.0, transition: minimizedVideoTransition)
|
||||
if self.animateRequestedVideoOnce {
|
||||
self.animateRequestedVideoOnce = false
|
||||
let videoButtonFrame = self.buttonsNode.videoButtonFrame().flatMap { frame -> CGRect in
|
||||
return self.buttonsNode.view.convert(frame, to: self.view)
|
||||
}
|
||||
|
||||
if let previousVideoButtonFrame = previousVideoButtonFrame, let videoButtonFrame = videoButtonFrame {
|
||||
minimizedVideoNode.animateRadialMask(from: previousVideoButtonFrame, to: videoButtonFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.animationForExpandedVideoSnapshotView = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public final class CallKitIntegration {
|
|||
return self.audioSessionActivePromise.get()
|
||||
}
|
||||
|
||||
init?(enableVideoCalls: Bool, startCall: @escaping (Account, UUID, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
init?(startCall: @escaping (Account, UUID, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
if !CallKitIntegration.isAvailable {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ public final class CallKitIntegration {
|
|||
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
if sharedProviderDelegate == nil {
|
||||
sharedProviderDelegate = CallKitProviderDelegate(enableVideoCalls: enableVideoCalls)
|
||||
sharedProviderDelegate = CallKitProviderDelegate()
|
||||
}
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged)
|
||||
} else {
|
||||
|
|
@ -112,8 +112,8 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
|
||||
fileprivate var audioSessionActivePromise: ValuePromise<Bool>?
|
||||
|
||||
init(enableVideoCalls: Bool) {
|
||||
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration(enableVideoCalls: enableVideoCalls))
|
||||
override init() {
|
||||
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration())
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
self.audioSessionActivationChanged = audioSessionActivationChanged
|
||||
}
|
||||
|
||||
private static func providerConfiguration(enableVideoCalls: Bool) -> CXProviderConfiguration {
|
||||
private static func providerConfiguration() -> CXProviderConfiguration {
|
||||
let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram")
|
||||
|
||||
providerConfiguration.supportsVideo = false
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
public let isOutgoing: Bool
|
||||
public var isVideo: Bool
|
||||
public var isVideoPossible: Bool
|
||||
public let enableHighBitrateVideoCalls: Bool
|
||||
public let peer: Peer?
|
||||
|
||||
private let serializedData: String?
|
||||
|
|
@ -257,7 +258,8 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
currentNetworkType: NetworkType,
|
||||
updatedNetworkType: Signal<NetworkType, NoError>,
|
||||
startWithVideo: Bool,
|
||||
isVideoPossible: Bool
|
||||
isVideoPossible: Bool,
|
||||
enableHighBitrateVideoCalls: Bool
|
||||
) {
|
||||
self.account = account
|
||||
self.audioSession = audioSession
|
||||
|
|
@ -286,6 +288,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
self.isVideoPossible = isVideoPossible
|
||||
self.peer = peer
|
||||
self.isVideo = startWithVideo
|
||||
self.enableHighBitrateVideoCalls = enableHighBitrateVideoCalls
|
||||
if self.isVideo {
|
||||
self.videoCapturer = OngoingCallVideoCapturer()
|
||||
self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: .outgoingRequested, remoteVideoState: .active))
|
||||
|
|
@ -463,8 +466,8 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
mappedVideoState = .possible
|
||||
case .outgoingRequested:
|
||||
mappedVideoState = .outgoingRequested
|
||||
case .incomingRequested:
|
||||
mappedVideoState = .incomingRequested
|
||||
case let .incomingRequested(sendsVideo):
|
||||
mappedVideoState = .incomingRequested(sendsVideo: sendsVideo)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
self.videoWasActive = true
|
||||
|
|
@ -569,7 +572,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||
let logName = "\(id.id)_\(id.accessHash)"
|
||||
|
||||
let ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, auxiliaryServers: auxiliaryServers, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, audioSessionActive: self.audioSessionActive.get(), logName: logName)
|
||||
let ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableHighBitrateVideoCalls: self.enableHighBitrateVideoCalls, audioSessionActive: self.audioSessionActive.get(), logName: logName)
|
||||
self.ongoingContext = ongoingContext
|
||||
ongoingContext.setIsMuted(self.isMutedValue)
|
||||
|
||||
|
|
@ -811,6 +814,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
if let view = view {
|
||||
let setOnFirstFrameReceived = view.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = view.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated
|
||||
completion(PresentationCallVideoView(
|
||||
view: view.view,
|
||||
setOnFirstFrameReceived: { f in
|
||||
|
|
@ -849,6 +853,11 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
}
|
||||
f?(mappedValue)
|
||||
}
|
||||
},
|
||||
setOnIsMirroredUpdated: { f in
|
||||
setOnIsMirroredUpdated { value in
|
||||
f?(value)
|
||||
}
|
||||
}
|
||||
))
|
||||
} else {
|
||||
|
|
@ -867,6 +876,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
if let view = view {
|
||||
let setOnFirstFrameReceived = view.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = view.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated
|
||||
completion(PresentationCallVideoView(
|
||||
view: view.view,
|
||||
setOnFirstFrameReceived: { f in
|
||||
|
|
@ -905,8 +915,12 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
}
|
||||
f?(mappedValue)
|
||||
}
|
||||
},
|
||||
setOnIsMirroredUpdated: { f in
|
||||
setOnIsMirroredUpdated { value in
|
||||
f?(value)
|
||||
}
|
||||
}
|
||||
|
||||
))
|
||||
} else {
|
||||
completion(nil)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
private let accountManager: AccountManager
|
||||
private let audioSession: ManagedAudioSession
|
||||
private let callKitIntegration: CallKitIntegration?
|
||||
private var isVideoPossible: Bool
|
||||
|
||||
private var currentCallValue: PresentationCallImpl?
|
||||
private var currentCall: PresentationCallImpl? {
|
||||
|
|
@ -117,15 +116,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
return OngoingCallContext.maxLayer
|
||||
}
|
||||
|
||||
public static func voipVersions(includeExperimental: Bool, includeReference: Bool) -> [String] {
|
||||
public static func voipVersions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] {
|
||||
return OngoingCallContext.versions(includeExperimental: includeExperimental, includeReference: includeReference)
|
||||
}
|
||||
|
||||
public init(accountManager: AccountManager, enableVideoCalls: Bool, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), isMediaPlaying: @escaping () -> Bool, resumeMediaPlayback: @escaping () -> Void, audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) {
|
||||
public init(accountManager: AccountManager, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), isMediaPlaying: @escaping () -> Bool, resumeMediaPlayback: @escaping () -> Void, audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) {
|
||||
self.getDeviceAccessData = getDeviceAccessData
|
||||
self.accountManager = accountManager
|
||||
self.audioSession = audioSession
|
||||
self.isVideoPossible = enableVideoCalls
|
||||
|
||||
self.isMediaPlaying = isMediaPlaying
|
||||
self.resumeMediaPlayback = resumeMediaPlayback
|
||||
|
|
@ -136,7 +134,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
var setCallMutedImpl: ((UUID, Bool) -> Void)?
|
||||
var audioSessionActivationChangedImpl: ((Bool) -> Void)?
|
||||
|
||||
self.callKitIntegration = CallKitIntegration(enableVideoCalls: enableVideoCalls, startCall: { account, uuid, handle, isVideo in
|
||||
self.callKitIntegration = CallKitIntegration(startCall: { account, uuid, handle, isVideo in
|
||||
if let startCallImpl = startCallImpl {
|
||||
return startCallImpl(account, uuid, handle, isVideo)
|
||||
} else {
|
||||
|
|
@ -214,7 +212,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
|
||||
startCallImpl = { [weak self] account, uuid, handle, isVideo in
|
||||
if let strongSelf = self, let userId = Int32(handle) {
|
||||
return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: isVideo, isVideoPossible: strongSelf.isVideoPossible, internalId: uuid)
|
||||
return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: isVideo, internalId: uuid)
|
||||
|> take(1)
|
||||
|> map { result -> Bool in
|
||||
return result
|
||||
|
|
@ -283,7 +281,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
private func ringingStatesUpdated(_ ringingStates: [(Account, Peer, CallSessionRingingState, Bool, NetworkType)], enableCallKit: Bool) {
|
||||
if let firstState = ringingStates.first {
|
||||
if self.currentCall == nil {
|
||||
self.currentCallDisposable.set((combineLatest(firstState.0.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
self.currentCallDisposable.set((combineLatest(firstState.0.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -292,6 +290,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
|
||||
let derivedState = preferences.values[ApplicationSpecificPreferencesKeys.voipDerivedState] as? VoipDerivedState ?? .default
|
||||
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings
|
||||
let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings ?? .defaultSettings
|
||||
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
|
||||
let call = PresentationCallImpl(
|
||||
|
|
@ -313,7 +312,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
currentNetworkType: firstState.4,
|
||||
updatedNetworkType: firstState.0.networkType,
|
||||
startWithVideo: firstState.2.isVideo,
|
||||
isVideoPossible: strongSelf.isVideoPossible
|
||||
isVideoPossible: firstState.2.isVideoPossible,
|
||||
enableHighBitrateVideoCalls: experimentalSettings.enableHighBitrateVideoCalls
|
||||
)
|
||||
strongSelf.updateCurrentCall(call)
|
||||
strongSelf.currentCallPromise.set(.single(call))
|
||||
|
|
@ -339,8 +339,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
}
|
||||
}
|
||||
|
||||
public func requestCall(account: Account, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult {
|
||||
let isVideoPossible = self.isVideoPossible
|
||||
public func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult {
|
||||
let account = context.account
|
||||
|
||||
if let call = self.currentCall, !endCurrentIfAny {
|
||||
return .alreadyInProgress(call.peerId)
|
||||
}
|
||||
|
|
@ -405,7 +406,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.startCall(account: account, peerId: peerId, isVideo: isVideo, isVideoPossible: isVideoPossible).start()
|
||||
let _ = strongSelf.startCall(account: account, peerId: peerId, isVideo: isVideo).start()
|
||||
}
|
||||
if let currentCall = self.currentCall {
|
||||
self.startCallDisposable.set((currentCall.hangUp()
|
||||
|
|
@ -423,7 +424,6 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
account: Account,
|
||||
peerId: PeerId,
|
||||
isVideo: Bool,
|
||||
isVideoPossible: Bool,
|
||||
internalId: CallSessionInternalId = CallSessionInternalId()
|
||||
) -> Signal<Bool, NoError> {
|
||||
let (presentationData, present, openSettings) = self.getDeviceAccessData()
|
||||
|
|
@ -459,9 +459,28 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
if !accessEnabled {
|
||||
return .single(false)
|
||||
}
|
||||
return (combineLatest(queue: .mainQueue(), account.callSessionManager.request(peerId: peerId, isVideo: isVideo, internalId: internalId), networkType |> take(1), account.postbox.peerView(id: peerId) |> map { peerView -> Bool in
|
||||
|
||||
let request = account.postbox.transaction { transaction -> VideoCallsConfiguration in
|
||||
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
return VideoCallsConfiguration(appConfiguration: appConfiguration)
|
||||
}
|
||||
|> mapToSignal { callsConfiguration -> Signal<CallSessionInternalId, NoError> in
|
||||
let isVideoPossible: Bool
|
||||
switch callsConfiguration.videoCallsSupport {
|
||||
case .disabled:
|
||||
isVideoPossible = isVideo
|
||||
case .full:
|
||||
isVideoPossible = true
|
||||
case .onlyVideo:
|
||||
isVideoPossible = isVideo
|
||||
}
|
||||
|
||||
return account.callSessionManager.request(peerId: peerId, isVideo: isVideo, enableVideo: isVideoPossible, internalId: internalId)
|
||||
}
|
||||
|
||||
return (combineLatest(queue: .mainQueue(), request, networkType |> take(1), account.postbox.peerView(id: peerId) |> map { peerView -> Bool in
|
||||
return peerView.peerIsContact
|
||||
} |> take(1), account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
} |> take(1), account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1))
|
||||
|> deliverOnMainQueue
|
||||
|> beforeNext { internalId, currentNetworkType, isContact, preferences, sharedData in
|
||||
if let strongSelf = self, accessEnabled {
|
||||
|
|
@ -474,6 +493,19 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings
|
||||
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
|
||||
let callsConfiguration = VideoCallsConfiguration(appConfiguration: appConfiguration)
|
||||
let isVideoPossible: Bool
|
||||
switch callsConfiguration.videoCallsSupport {
|
||||
case .disabled:
|
||||
isVideoPossible = isVideo
|
||||
case .full:
|
||||
isVideoPossible = true
|
||||
case .onlyVideo:
|
||||
isVideoPossible = isVideo
|
||||
}
|
||||
|
||||
let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings ?? .defaultSettings
|
||||
|
||||
let call = PresentationCallImpl(
|
||||
account: account,
|
||||
audioSession: strongSelf.audioSession,
|
||||
|
|
@ -496,7 +528,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
currentNetworkType: currentNetworkType,
|
||||
updatedNetworkType: account.networkType,
|
||||
startWithVideo: isVideo,
|
||||
isVideoPossible: isVideoPossible
|
||||
isVideoPossible: isVideoPossible,
|
||||
enableHighBitrateVideoCalls: experimentalSettings.enableHighBitrateVideoCalls
|
||||
)
|
||||
strongSelf.updateCurrentCall(call)
|
||||
strongSelf.currentCallPromise.set(.single(call))
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ enum AccountStateMutationOperation {
|
|||
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
|
||||
case ReadMessageContents((PeerId?, [Int32]))
|
||||
case UpdateMessageImpressionCount(MessageId, Int32)
|
||||
case UpdateMessageForwardsCount(MessageId, Int32)
|
||||
case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation)
|
||||
case UpdateRecentGifs
|
||||
case UpdateChatInputState(PeerId, SynchronizeableChatInputState?)
|
||||
|
|
@ -435,6 +436,10 @@ struct AccountMutableState {
|
|||
self.addOperation(.UpdateMessageImpressionCount(id, count))
|
||||
}
|
||||
|
||||
mutating func addUpdateMessageForwardsCount(id: MessageId, count: Int32) {
|
||||
self.addOperation(.UpdateMessageForwardsCount(id, count))
|
||||
}
|
||||
|
||||
mutating func addUpdateInstalledStickerPacks(_ operation: AccountStateUpdateStickerPacksOperation) {
|
||||
self.addOperation(.UpdateInstalledStickerPacks(operation))
|
||||
}
|
||||
|
|
@ -469,7 +474,7 @@ struct AccountMutableState {
|
|||
|
||||
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
||||
switch operation {
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter:
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter:
|
||||
break
|
||||
case let .AddMessages(messages, location):
|
||||
for message in messages {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private var declaredEncodables: Void = {
|
|||
declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) })
|
||||
declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) })
|
||||
declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) })
|
||||
declareEncodable(ForwardCountMessageAttribute.self, f: { ForwardCountMessageAttribute(decoder: $0) })
|
||||
declareEncodable(NotificationInfoMessageAttribute.self, f: { NotificationInfoMessageAttribute(decoder: $0) })
|
||||
declareEncodable(TelegramMediaAction.self, f: { TelegramMediaAction(decoder: $0) })
|
||||
declareEncodable(TelegramPeerNotificationSettings.self, f: { TelegramPeerNotificationSettings(decoder: $0) })
|
||||
|
|
|
|||
|
|
@ -1253,6 +1253,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
|
|||
updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), messages))
|
||||
case let .updateChannelMessageViews(channelId, id, views):
|
||||
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: views)
|
||||
/*case let .updateChannelMessageForwards(channelId, id, forwards):
|
||||
updatedState.addUpdateMessageForwardsCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: forwards)*/
|
||||
case let .updateNewStickerSet(stickerset):
|
||||
updatedState.addUpdateInstalledStickerPacks(.add(stickerset))
|
||||
case let .updateStickerSetsOrder(flags, order):
|
||||
|
|
@ -1896,6 +1898,8 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat
|
|||
updatedState.addReadMessagesContents((peer.id, messages))
|
||||
case let .updateChannelMessageViews(_, id, views):
|
||||
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)
|
||||
/*case let .updateChannelMessageForwards(_, id, views):
|
||||
updatedState.addUpdateMessageForwardsCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)*/
|
||||
case let .updateChannelWebPage(_, apiWebpage, _, _):
|
||||
switch apiWebpage {
|
||||
case let .webPageEmpty(id):
|
||||
|
|
@ -2090,7 +2094,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
|
|||
var currentAddScheduledMessages: OptimizeAddMessagesState?
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder:
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder:
|
||||
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
|
||||
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
|
||||
}
|
||||
|
|
@ -2773,6 +2777,21 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
|||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
case let .UpdateMessageForwardsCount(id, count):
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
if let attribute = attributes[j] as? ForwardCountMessageAttribute {
|
||||
attributes[j] = ForwardCountMessageAttribute(count: max(attribute.count, Int(count)))
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
case let .UpdateInstalledStickerPacks(operation):
|
||||
stickerPackOperations.append(operation)
|
||||
case .UpdateRecentGifs:
|
||||
|
|
|
|||
|
|
@ -599,20 +599,34 @@ public final class AccountViewTracker {
|
|||
return account.postbox.transaction { transaction -> Void in
|
||||
for i in 0 ..< messageIds.count {
|
||||
if i < viewCounts.count {
|
||||
transaction.updateMessage(messageIds[i], update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
if let attribute = attributes[j] as? ViewCountMessageAttribute {
|
||||
if attribute.count >= Int(viewCounts[i]) {
|
||||
return .skip
|
||||
let views = viewCounts[i]
|
||||
do {
|
||||
transaction.updateMessage(messageIds[i], update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
if let attribute = attributes[j] as? ViewCountMessageAttribute {
|
||||
attributes[j] = ViewCountMessageAttribute(count: max(attribute.count, Int(views)))
|
||||
}
|
||||
attributes[j] = ViewCountMessageAttribute(count: max(attribute.count, Int(viewCounts[i])))
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
}
|
||||
/*if case let .messageViews(views, forwards) = viewCounts[i] {
|
||||
transaction.updateMessage(messageIds[i], update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
if let attribute = attributes[j] as? ViewCountMessageAttribute {
|
||||
attributes[j] = ViewCountMessageAttribute(count: max(attribute.count, Int(views)))
|
||||
}
|
||||
if let _ = attributes[j] as? ForwardCountMessageAttribute {
|
||||
attributes[j] = ForwardCountMessageAttribute(count: Int(forwards))
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ func imageRepresentationsForApiChatPhoto(_ photo: Api.ChatPhoto) -> [TelegramMed
|
|||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: .fullSize, volumeId: volumeId, localId: localId)
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: []))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: []))
|
||||
case .chatPhotoEmpty:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,10 +108,7 @@ public struct CallSessionRingingState: Equatable {
|
|||
public let id: CallSessionInternalId
|
||||
public let peerId: PeerId
|
||||
public let isVideo: Bool
|
||||
|
||||
public static func ==(lhs: CallSessionRingingState, rhs: CallSessionRingingState) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.peerId == rhs.peerId && lhs.isVideo == rhs.isVideo
|
||||
}
|
||||
public let isVideoPossible: Bool
|
||||
}
|
||||
|
||||
public enum DropCallReason {
|
||||
|
|
@ -191,18 +188,79 @@ public struct CallSession {
|
|||
public let state: CallSessionState
|
||||
}
|
||||
|
||||
public struct CallSessionConnection {
|
||||
public let id: Int64
|
||||
public let ip: String
|
||||
public let ipv6: String
|
||||
public let port: Int32
|
||||
public let peerTag: Data
|
||||
public enum CallSessionConnection: Equatable {
|
||||
public struct Reflector: Equatable {
|
||||
public let id: Int64
|
||||
public let ip: String
|
||||
public let ipv6: String
|
||||
public let port: Int32
|
||||
public let peerTag: Data
|
||||
|
||||
public init(
|
||||
id: Int64,
|
||||
ip: String,
|
||||
ipv6: String,
|
||||
port: Int32,
|
||||
peerTag: Data
|
||||
) {
|
||||
self.id = id
|
||||
self.ip = ip
|
||||
self.ipv6 = ipv6
|
||||
self.port = port
|
||||
self.peerTag = peerTag
|
||||
}
|
||||
}
|
||||
|
||||
public struct WebRtcReflector: Equatable {
|
||||
public let id: Int64
|
||||
public let hasStun: Bool
|
||||
public let hasTurn: Bool
|
||||
public let ip: String
|
||||
public let ipv6: String
|
||||
public let port: Int32
|
||||
public let username: String
|
||||
public let password: String
|
||||
|
||||
public init(
|
||||
id: Int64,
|
||||
hasStun: Bool,
|
||||
hasTurn: Bool,
|
||||
ip: String,
|
||||
ipv6: String,
|
||||
port: Int32,
|
||||
username: String,
|
||||
password: String
|
||||
) {
|
||||
self.id = id
|
||||
self.hasStun = hasStun
|
||||
self.hasTurn = hasTurn
|
||||
self.ip = ip
|
||||
self.ipv6 = ipv6
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
}
|
||||
}
|
||||
|
||||
case reflector(Reflector)
|
||||
case webRtcReflector(WebRtcReflector)
|
||||
}
|
||||
|
||||
private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessionConnection {
|
||||
switch apiConnection {
|
||||
case let .phoneConnection(id, ip, ipv6, port, peerTag):
|
||||
return CallSessionConnection(id: id, ip: ip, ipv6: ipv6, port: port, peerTag: peerTag.makeData())
|
||||
return .reflector(CallSessionConnection.Reflector(id: id, ip: ip, ipv6: ipv6, port: port, peerTag: peerTag.makeData()))
|
||||
case let .phoneConnectionWebrtc(flags, id, ip, ipv6, port, username, password):
|
||||
return .webRtcReflector(CallSessionConnection.WebRtcReflector(
|
||||
id: id,
|
||||
hasStun: (flags & (1 << 1)) != 0,
|
||||
hasTurn: (flags & (1 << 0)) != 0,
|
||||
ip: ip,
|
||||
ipv6: ipv6,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +277,7 @@ private final class CallSessionContext {
|
|||
let peerId: PeerId
|
||||
let isOutgoing: Bool
|
||||
var type: CallSession.CallType
|
||||
let isVideoPossible: Bool
|
||||
var state: CallSessionInternalState
|
||||
let subscribers = Bag<(CallSession) -> Void>()
|
||||
let signalingSubscribers = Bag<(Data) -> Void>()
|
||||
|
|
@ -235,10 +294,11 @@ private final class CallSessionContext {
|
|||
}
|
||||
}
|
||||
|
||||
init(peerId: PeerId, isOutgoing: Bool, type: CallSession.CallType, state: CallSessionInternalState) {
|
||||
init(peerId: PeerId, isOutgoing: Bool, type: CallSession.CallType, isVideoPossible: Bool, state: CallSessionInternalState) {
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.type = type
|
||||
self.isVideoPossible = isVideoPossible
|
||||
self.state = state
|
||||
}
|
||||
|
||||
|
|
@ -247,8 +307,8 @@ private final class CallSessionContext {
|
|||
}
|
||||
}
|
||||
|
||||
private func selectVersionOnAccept(localVersions: [String], remoteVersions: [String]) -> [String]? {
|
||||
let filteredVersions = localVersions.filter(remoteVersions.contains)
|
||||
private func selectVersionOnAccept(localVersions: [CallSessionManagerImplementationVersion], remoteVersions: [String]) -> [String]? {
|
||||
let filteredVersions = localVersions.map(\.version).filter(remoteVersions.contains)
|
||||
if filteredVersions.isEmpty {
|
||||
return nil
|
||||
} else {
|
||||
|
|
@ -256,12 +316,22 @@ private func selectVersionOnAccept(localVersions: [String], remoteVersions: [Str
|
|||
}
|
||||
}
|
||||
|
||||
public struct CallSessionManagerImplementationVersion: Hashable {
|
||||
public var version: String
|
||||
public var supportsVideo: Bool
|
||||
|
||||
public init(version: String, supportsVideo: Bool) {
|
||||
self.version = version
|
||||
self.supportsVideo = supportsVideo
|
||||
}
|
||||
}
|
||||
|
||||
private final class CallSessionManagerContext {
|
||||
private let queue: Queue
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
private let maxLayer: Int32
|
||||
private var versions: [String]
|
||||
private var versions: [CallSessionManagerImplementationVersion]
|
||||
private let addUpdates: (Api.Updates) -> Void
|
||||
|
||||
private let ringingSubscribers = Bag<([CallSessionRingingState]) -> Void>()
|
||||
|
|
@ -270,7 +340,7 @@ private final class CallSessionManagerContext {
|
|||
|
||||
private let disposables = DisposableSet()
|
||||
|
||||
init(queue: Queue, postbox: Postbox, network: Network, maxLayer: Int32, versions: [String], addUpdates: @escaping (Api.Updates) -> Void) {
|
||||
init(queue: Queue, postbox: Postbox, network: Network, maxLayer: Int32, versions: [CallSessionManagerImplementationVersion], addUpdates: @escaping (Api.Updates) -> Void) {
|
||||
self.queue = queue
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
|
|
@ -284,10 +354,32 @@ private final class CallSessionManagerContext {
|
|||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
func updateVersions(versions: [String]) {
|
||||
func updateVersions(versions: [CallSessionManagerImplementationVersion]) {
|
||||
self.versions = versions.reversed()
|
||||
}
|
||||
|
||||
func filteredVersions(enableVideo: Bool) -> [String] {
|
||||
return self.versions.compactMap { version -> String? in
|
||||
if enableVideo {
|
||||
return version.version
|
||||
} else if !version.supportsVideo {
|
||||
return version.version
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func videoVersions() -> [String] {
|
||||
return self.versions.compactMap { version -> String? in
|
||||
if version.supportsVideo {
|
||||
return version.version
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ringingStates() -> Signal<[CallSessionRingingState], NoError> {
|
||||
let queue = self.queue
|
||||
return Signal { [weak self] subscriber in
|
||||
|
|
@ -366,7 +458,7 @@ private final class CallSessionManagerContext {
|
|||
var ringingContexts: [CallSessionRingingState] = []
|
||||
for (id, context) in self.contexts {
|
||||
if case .ringing = context.state {
|
||||
ringingContexts.append(CallSessionRingingState(id: id, peerId: context.peerId, isVideo: context.type == .video))
|
||||
ringingContexts.append(CallSessionRingingState(id: id, peerId: context.peerId, isVideo: context.type == .video, isVideoPossible: context.isVideoPossible))
|
||||
}
|
||||
}
|
||||
return ringingContexts
|
||||
|
|
@ -398,8 +490,13 @@ private final class CallSessionManagerContext {
|
|||
let b = Data(bytesNoCopy: bBytes, count: 256, deallocator: .free)
|
||||
|
||||
if randomStatus == 0 {
|
||||
var isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) })
|
||||
//#if DEBUG
|
||||
isVideoPossible = true
|
||||
//#endif
|
||||
|
||||
let internalId = CallSessionInternalId()
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, type: isVideo ? .video : .audio, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions))
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, type: isVideo ? .video : .audio, isVideoPossible: isVideoPossible, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions))
|
||||
self.contexts[internalId] = context
|
||||
let queue = self.queue
|
||||
context.acknowledgeIncomingCallDisposable.set(self.network.request(Api.functions.phone.receivedCall(peer: .inputPhoneCall(id: stableId, accessHash: accessHash))).start(error: { [weak self] _ in
|
||||
|
|
@ -529,11 +626,7 @@ private final class CallSessionManagerContext {
|
|||
if let context = self.contexts[internalId] {
|
||||
switch context.state {
|
||||
case let .ringing(id, accessHash, gAHash, b, remoteVersions):
|
||||
guard var acceptVersions = selectVersionOnAccept(localVersions: self.versions, remoteVersions: remoteVersions) else {
|
||||
self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil))
|
||||
return
|
||||
}
|
||||
acceptVersions = self.versions
|
||||
let acceptVersions = self.versions.map(\.version)
|
||||
context.state = .accepting(id: id, accessHash: accessHash, gAHash: gAHash, b: b, disposable: (acceptCallSession(postbox: self.postbox, network: self.network, stableId: id, accessHash: accessHash, b: b, maxLayer: self.maxLayer, versions: acceptVersions) |> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
if let strongSelf = self, let context = strongSelf.contexts[internalId] {
|
||||
if case .accepting = context.state {
|
||||
|
|
@ -738,7 +831,7 @@ private final class CallSessionManagerContext {
|
|||
}
|
||||
}
|
||||
case let .phoneCallRequested(flags, id, accessHash, date, adminId, _, gAHash, requestedProtocol):
|
||||
let isVideo = (flags & (1 << 5)) != 0
|
||||
let isVideo = (flags & (1 << 6)) != 0
|
||||
let versions: [String]
|
||||
switch requestedProtocol {
|
||||
case let .phoneCallProtocol(_, _, _, libraryVersions):
|
||||
|
|
@ -819,12 +912,12 @@ private final class CallSessionManagerContext {
|
|||
return (key, keyId, keyVisualHash)
|
||||
}
|
||||
|
||||
func request(peerId: PeerId, internalId: CallSessionInternalId, isVideo: Bool) -> CallSessionInternalId? {
|
||||
func request(peerId: PeerId, internalId: CallSessionInternalId, isVideo: Bool, enableVideo: Bool) -> CallSessionInternalId? {
|
||||
let aBytes = malloc(256)!
|
||||
let randomStatus = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self))
|
||||
let a = Data(bytesNoCopy: aBytes, count: 256, deallocator: .free)
|
||||
if randomStatus == 0 {
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.versions, isVideo: isVideo) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: enableVideo), isVideo: isVideo) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
if let strongSelf = self, let context = strongSelf.contexts[internalId] {
|
||||
if case .requesting = context.state {
|
||||
switch result {
|
||||
|
|
@ -858,7 +951,7 @@ public final class CallSessionManager {
|
|||
private let queue = Queue()
|
||||
private var contextRef: Unmanaged<CallSessionManagerContext>?
|
||||
|
||||
init(postbox: Postbox, network: Network, maxLayer: Int32, versions: [String], addUpdates: @escaping (Api.Updates) -> Void) {
|
||||
init(postbox: Postbox, network: Network, maxLayer: Int32, versions: [CallSessionManagerImplementationVersion], addUpdates: @escaping (Api.Updates) -> Void) {
|
||||
self.queue.async {
|
||||
let context = CallSessionManagerContext(queue: self.queue, postbox: postbox, network: network, maxLayer: maxLayer, versions: versions, addUpdates: addUpdates)
|
||||
self.contextRef = Unmanaged.passRetained(context)
|
||||
|
|
@ -917,12 +1010,12 @@ public final class CallSessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public func request(peerId: PeerId, isVideo: Bool, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<CallSessionInternalId, NoError> {
|
||||
public func request(peerId: PeerId, isVideo: Bool, enableVideo: Bool, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<CallSessionInternalId, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self?.withContext { context in
|
||||
if let internalId = context.request(peerId: peerId, internalId: internalId, isVideo: isVideo) {
|
||||
if let internalId = context.request(peerId: peerId, internalId: internalId, isVideo: isVideo, enableVideo: enableVideo) {
|
||||
subscriber.putNext(internalId)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
|
@ -944,7 +1037,7 @@ public final class CallSessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public func updateVersions(versions: [String]) {
|
||||
public func updateVersions(versions: [CallSessionManagerImplementationVersion]) {
|
||||
self.withContext { context in
|
||||
context.updateVersions(versions: versions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@ public func imageRepresentationLargerThan(_ representations: [TelegramMediaImage
|
|||
}
|
||||
}
|
||||
|
||||
public func progressiveImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? {
|
||||
for representation in representations {
|
||||
if representation.progressiveSizes.count > 1 {
|
||||
return representation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseMediaData(data: Data) -> Media? {
|
||||
let buffer = BufferReader(Buffer(data: data))
|
||||
var parseBuffer: Buffer?
|
||||
|
|
|
|||
188
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
188
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
import SyncCore
|
||||
|
||||
public struct MessageStats: Equatable {
|
||||
public let views: Int
|
||||
public let forwards: Int
|
||||
public let interactionsGraph: StatsGraph
|
||||
|
||||
init(views: Int, forwards: Int, interactionsGraph: StatsGraph) {
|
||||
self.views = views
|
||||
self.forwards = forwards
|
||||
self.interactionsGraph = interactionsGraph
|
||||
}
|
||||
|
||||
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
|
||||
if lhs.views != rhs.views {
|
||||
return false
|
||||
}
|
||||
if lhs.forwards != rhs.forwards {
|
||||
return false
|
||||
}
|
||||
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
||||
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: self.interactionsGraph)
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageStatsContextState: Equatable {
|
||||
public var stats: MessageStats?
|
||||
}
|
||||
|
||||
private func requestMessageStats(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId, dark: Bool = false) -> Signal<MessageStats?, NoError> {
|
||||
return .single(nil)
|
||||
/*return postbox.transaction { transaction -> (Peer, Message)? in
|
||||
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId) {
|
||||
return (peer, message)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} |> mapToSignal { peerAndMessage -> Signal<MessageStats?, NoError> in
|
||||
guard let (peer, message) = peerAndMessage, let inputChannel = apiInputChannel(peer) else {
|
||||
return .never()
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if dark {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getMessageStats(flags: flags, channel: inputChannel, msgId: messageId.id)
|
||||
let signal: Signal<Api.stats.MessageStats, MTRpcError>
|
||||
if network.datacenterId != datacenterId {
|
||||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(request)
|
||||
}
|
||||
} else {
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
var views: Int = 0
|
||||
var forwards: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||
views = viewsAttribute.count
|
||||
} else if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
|
||||
forwards = forwardsAttribute.count
|
||||
}
|
||||
}
|
||||
|
||||
return signal
|
||||
|> map { result -> MessageStats? in
|
||||
if case let .messageStats(apiViewsGraph) = result {
|
||||
return MessageStats(views: views, forwards: forwards, interactionsGraph: StatsGraph(apiStatsGraph: apiViewsGraph))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> retryRequest
|
||||
}*/
|
||||
}
|
||||
|
||||
private final class MessageStatsContextImpl {
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
private let datacenterId: Int32
|
||||
private let messageId: MessageId
|
||||
|
||||
private var _state: MessageStatsContextState {
|
||||
didSet {
|
||||
if self._state != oldValue {
|
||||
self._statePromise.set(.single(self._state))
|
||||
}
|
||||
}
|
||||
}
|
||||
private let _statePromise = Promise<MessageStatsContextState>()
|
||||
var state: Signal<MessageStatsContextState, NoError> {
|
||||
return self._statePromise.get()
|
||||
}
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private let disposables = DisposableDict<String>()
|
||||
|
||||
init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
self.datacenterId = datacenterId
|
||||
self.messageId = messageId
|
||||
self._state = MessageStatsContextState(stats: nil)
|
||||
self._statePromise.set(.single(self._state))
|
||||
|
||||
self.load()
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
self.disposable.dispose()
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
private func load() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.disposable.set((requestMessageStats(postbox: self.postbox, network: self.network, datacenterId: self.datacenterId, messageId: self.messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stats in
|
||||
if let strongSelf = self {
|
||||
strongSelf._state = MessageStatsContextState(stats: stats)
|
||||
strongSelf._statePromise.set(.single(strongSelf._state))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||
if let token = graph.token {
|
||||
return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageStatsContext {
|
||||
private let impl: QueueLocalObject<MessageStatsContextImpl>
|
||||
|
||||
public var state: Signal<MessageStatsContextState, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.state.start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
|
||||
return MessageStatsContextImpl(postbox: postbox, network: network, datacenterId: datacenterId, messageId: messageId)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,7 +400,7 @@ private enum FetchResourceReference {
|
|||
|
||||
private final class MultipartFetchManager {
|
||||
let parallelParts: Int
|
||||
let defaultPartSize = 128 * 1024
|
||||
let defaultPartSize: Int
|
||||
var partAlignment = 4 * 1024
|
||||
|
||||
var resource: TelegramMediaResource
|
||||
|
|
@ -448,10 +448,17 @@ private final class MultipartFetchManager {
|
|||
self.consumerId = arc4random64()
|
||||
|
||||
self.completeSize = size
|
||||
if let _ = size {
|
||||
self.parallelParts = 4
|
||||
if let size = size {
|
||||
if size <= 512 * 1024 {
|
||||
self.defaultPartSize = 16 * 1024
|
||||
self.parallelParts = 4 * 4
|
||||
} else {
|
||||
self.defaultPartSize = 128 * 1024
|
||||
self.parallelParts = 4
|
||||
}
|
||||
} else {
|
||||
self.parallelParts = 1
|
||||
self.defaultPartSize = 128 * 1024
|
||||
}
|
||||
|
||||
if let info = parameters?.info as? TelegramCloudMediaResourceFetchInfo {
|
||||
|
|
|
|||
|
|
@ -403,12 +403,12 @@ public struct NetworkInitializationArguments {
|
|||
public let languagesCategory: String
|
||||
public let appVersion: String
|
||||
public let voipMaxLayer: Int32
|
||||
public let voipVersions: [String]
|
||||
public let voipVersions: [CallSessionManagerImplementationVersion]
|
||||
public let appData: Signal<Data?, NoError>
|
||||
public let autolockDeadine: Signal<Int32?, NoError>
|
||||
public let encryptionProvider: EncryptionProvider
|
||||
|
||||
public init(apiId: Int32, apiHash: String, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, voipVersions: [String], appData: Signal<Data?, NoError>, autolockDeadine: Signal<Int32?, NoError>, encryptionProvider: EncryptionProvider) {
|
||||
public init(apiId: Int32, apiHash: String, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, voipVersions: [CallSessionManagerImplementationVersion], appData: Signal<Data?, NoError>, autolockDeadine: Signal<Int32?, NoError>, encryptionProvider: EncryptionProvider) {
|
||||
self.apiId = apiId
|
||||
self.apiHash = apiHash
|
||||
self.languagesCategory = languagesCategory
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha
|
|||
arc4random_buf(&randomId, 8)
|
||||
let thumbnailResource = thumbnail.resource
|
||||
let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128)
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [])], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: tmpImage), replyToMessageId: nil, localGroupingKey: nil)
|
||||
} else {
|
||||
return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)
|
||||
|
|
@ -71,7 +71,7 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha
|
|||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource))
|
||||
} else {
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: []))
|
||||
}
|
||||
}
|
||||
var fileName = "file"
|
||||
|
|
|
|||
|
|
@ -161,7 +161,12 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan
|
|||
case let .photoSize(_, location, w, h, _):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: volumeId, localId: localId)))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: volumeId, localId: localId), progressiveSizes: []))
|
||||
}
|
||||
case let .photoSizeProgressive(_, location, w, h, sizes):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: volumeId, localId: localId), progressiveSizes: sizes))
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ public enum StatsGraph: Equatable {
|
|||
switch self {
|
||||
case .Empty:
|
||||
return true
|
||||
case let .Failed(error):
|
||||
return error.lowercased().contains("not enough data")
|
||||
case .Failed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
@ -193,15 +193,16 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
|||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)
|
||||
let signal: Signal<Api.stats.BroadcastStats, MTRpcError>
|
||||
if network.datacenterId != datacenterId {
|
||||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
return worker.request(request)
|
||||
}
|
||||
} else {
|
||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
return signal
|
||||
|
|
@ -212,7 +213,7 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
|||
}
|
||||
}
|
||||
|
||||
private func requestGraph(network: Network, datacenterId: Int32, token: String, x: Int64? = nil) -> Signal<StatsGraph?, NoError> {
|
||||
func requestGraph(network: Network, datacenterId: Int32, token: String, x: Int64? = nil) -> Signal<StatsGraph?, NoError> {
|
||||
var flags: Int32 = 0
|
||||
if let _ = x {
|
||||
flags |= (1 << 0)
|
||||
|
|
|
|||
|
|
@ -708,10 +708,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size)))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: []))
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
parsedMedia.append(image)
|
||||
}
|
||||
|
|
@ -734,7 +734,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -749,7 +749,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -767,7 +767,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .photoSize(_, location, w, h, size):
|
||||
switch location {
|
||||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil)))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
@ -777,7 +777,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil)
|
||||
resources.append((resource, bytes.makeData()))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
@ -906,10 +906,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size)))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: []))
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
parsedMedia.append(image)
|
||||
}
|
||||
|
|
@ -933,7 +933,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -964,7 +964,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -982,7 +982,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .photoSize(_, location, w, h, size):
|
||||
switch location {
|
||||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil)))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
@ -992,7 +992,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil)
|
||||
resources.append((resource, bytes.makeData()))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
@ -1140,10 +1140,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size)))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: []))
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
parsedMedia.append(image)
|
||||
}
|
||||
|
|
@ -1167,7 +1167,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -1198,7 +1198,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if thumb.size != 0 {
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: []))
|
||||
resources.append((resource, thumb.makeData()))
|
||||
}
|
||||
let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes)
|
||||
|
|
@ -1216,7 +1216,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .photoSize(_, location, w, h, size):
|
||||
switch location {
|
||||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil)))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
@ -1226,7 +1226,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
|
|||
case let .fileLocation(dcId, volumeId, localId, secret):
|
||||
let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil)
|
||||
resources.append((resource, bytes.makeData()))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
case .fileLocationUnavailable:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public enum SearchMessagesLocation: Equatable {
|
|||
case general
|
||||
case group(PeerGroupId)
|
||||
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?)
|
||||
case publicForwards(messageId: MessageId, datacenterId: Int?)
|
||||
}
|
||||
|
||||
private struct SearchMessagesPeerState: Equatable {
|
||||
|
|
@ -282,7 +283,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
|
|||
}
|
||||
}
|
||||
|> mapToSignal { (nextRate, lowerBound, inputPeer) in
|
||||
account.network.request(Api.functions.messages.searchGlobal(flags: 0, folderId: nil, q: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false)
|
||||
return account.network.request(Api.functions.messages.searchGlobal(flags: 0, folderId: nil, q: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false)
|
||||
|> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in
|
||||
return (result, nil)
|
||||
}
|
||||
|
|
@ -290,6 +291,46 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
|
|||
return .single((nil, nil))
|
||||
}
|
||||
}
|
||||
case let .publicForwards(messageId, datacenterId):
|
||||
remoteSearchResult = .single((nil, nil))
|
||||
/*remoteSearchResult = account.postbox.transaction { transaction -> (Api.InputChannel?, Int32, MessageIndex?, Api.InputPeer) in
|
||||
let sourcePeer = transaction.getPeer(messageId.peerId)
|
||||
let inputChannel = sourcePeer.flatMap { apiInputChannel($0) }
|
||||
|
||||
var lowerBound: MessageIndex?
|
||||
if let state = state, let message = state.main.messages.last {
|
||||
lowerBound = message.index
|
||||
}
|
||||
if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) {
|
||||
return (inputChannel, state?.main.nextRate ?? 0, lowerBound, inputPeer)
|
||||
} else {
|
||||
return (inputChannel, 0, lowerBound, .inputPeerEmpty)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { (inputChannel, nextRate, lowerBound, inputPeer) in
|
||||
guard let inputChannel = inputChannel else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getMessagePublicForwards(channel: inputChannel, msgId: messageId.id, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit)
|
||||
let signal: Signal<Api.messages.Messages, MTRpcError>
|
||||
if let datacenterId = datacenterId, account.network.datacenterId != datacenterId {
|
||||
signal = account.network.download(datacenterId: datacenterId, isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(request)
|
||||
}
|
||||
} else {
|
||||
signal = account.network.request(request, automaticFloodWait: false)
|
||||
}
|
||||
return signal
|
||||
|> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in
|
||||
return (result, nil)
|
||||
}
|
||||
|> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in
|
||||
return .single((nil, nil))
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
return remoteSearchResult
|
||||
|
|
@ -572,11 +613,11 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, timesta
|
|||
} |> switchToLatest
|
||||
}
|
||||
|
||||
enum UpdatedRemotePeerError {
|
||||
public enum UpdatedRemotePeerError {
|
||||
case generic
|
||||
}
|
||||
|
||||
func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal<Peer, UpdatedRemotePeerError> {
|
||||
public func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal<Peer, UpdatedRemotePeerError> {
|
||||
if let inputUser = peer.inputUser {
|
||||
return network.request(Api.functions.users.getUsers(id: [inputUser]))
|
||||
|> mapError { _ -> UpdatedRemotePeerError in
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
|||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 116
|
||||
return 117
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ public func standaloneUploadedImage(account: Account, peerId: PeerId, text: Stri
|
|||
|> mapToSignal { result -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
|
||||
switch result {
|
||||
case let .encryptedFile(id, accessHash, size, dcId, _):
|
||||
return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int32(data.count), datacenterId: Int(dcId), key: key))], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])))))
|
||||
return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int32(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [])], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])))))
|
||||
case .encryptedFileEmpty:
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,19 @@ func telegramStickerPachThumbnailRepresentationFromApiSize(datacenterId: Int32,
|
|||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId)
|
||||
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource)
|
||||
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])
|
||||
}
|
||||
case let .photoSize(_, location, w, h, _):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId)
|
||||
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource)
|
||||
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])
|
||||
}
|
||||
case let .photoSizeProgressive(_, location, w, h, sizes):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId)
|
||||
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes)
|
||||
}
|
||||
case .photoStrippedSize:
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -521,6 +521,10 @@ extension StoreMessage {
|
|||
attributes.append(ViewCountMessageAttribute(count: Int(views)))
|
||||
}
|
||||
|
||||
/*if let forwards = forwards, namespace != Namespaces.Message.ScheduledCloud {
|
||||
attributes.append(ForwardCountMessageAttribute(count: Int(forwards)))
|
||||
}*/
|
||||
|
||||
if let editDate = editDate {
|
||||
attributes.append(EditedMessageAttribute(date: editDate, isHidden: (flags & (1 << 21)) != 0))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,13 +129,19 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32,
|
|||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
case let .photoSize(type, location, w, h, _):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
case let .photoSizeProgressive(type, location, w, h, sizes):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes))
|
||||
}
|
||||
case let .photoStrippedSize(_, data):
|
||||
immediateThumbnailData = data.makeData()
|
||||
|
|
|
|||
|
|
@ -13,13 +13,21 @@ func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId:
|
|||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: nil, fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
case let .photoSize(type, location, w, h, size):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(size), fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: []))
|
||||
}
|
||||
case let .photoSizeProgressive(type, location, w, h, sizes):
|
||||
switch location {
|
||||
case let .fileLocationToBeDeprecated(volumeId, localId):
|
||||
if !sizes.isEmpty {
|
||||
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(sizes[sizes.count - 1]), fileReference: fileReference)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes))
|
||||
}
|
||||
}
|
||||
case let .photoStrippedSize(_, data):
|
||||
immediateThumbnailData = data.makeData()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue