Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,169 @@
import Foundation
import UIKit
import Display
import TelegramCore
import MapKit
import SwiftSignalKit
public struct MapSnapshotMediaResourceId {
public let latitude: Double
public let longitude: Double
public let width: Int32
public let height: Int32
public var uniqueId: String {
return "map-\(latitude)-\(longitude)-\(width)x\(height)"
}
public var hashValue: Int {
return self.uniqueId.hashValue
}
}
public class MapSnapshotMediaResource {
public let latitude: Double
public let longitude: Double
public let width: Int32
public let height: Int32
public init(latitude: Double, longitude: Double, width: Int32, height: Int32) {
self.latitude = latitude
self.longitude = longitude
self.width = width
self.height = height
}
public var id: EngineMediaResource.Id {
return EngineMediaResource.Id(MapSnapshotMediaResourceId(latitude: self.latitude, longitude: self.longitude, width: self.width, height: self.height).uniqueId)
}
}
let TGGoogleMapsOffset: Int = 268435456
let TGGoogleMapsRadius = Double(TGGoogleMapsOffset) / Double.pi
private func yToLatitude(_ y: Int) -> Double {
return ((Double.pi / 2.0) - 2 * atan(exp((Double(y - TGGoogleMapsOffset)) / TGGoogleMapsRadius))) * 180.0 / Double.pi;
}
private func latitudeToY(_ latitude: Double) -> Int {
return Int(round(Double(TGGoogleMapsOffset) - TGGoogleMapsRadius * log((1.0 + sin(latitude * Double.pi / 180.0)) / (1.0 - sin(latitude * Double.pi / 180.0))) / 2.0))
}
private func adjustGMapLatitude(_ latitude: Double, offset: Int, zoom: Int) -> Double {
let t: Int = (offset << (21 - zoom))
return yToLatitude(latitudeToY(latitude) + t)
}
private func fetchMapSnapshotResource(resource: MapSnapshotMediaResource) -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> {
return Signal { subscriber in
let disposable = MetaDisposable()
Queue.concurrentDefaultQueue().async {
let options = MKMapSnapshotter.Options()
let latitude = adjustGMapLatitude(resource.latitude, offset: -10, zoom: 15)
options.region = MKCoordinateRegion(center: CLLocationCoordinate2DMake(latitude, resource.longitude), span: MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003))
options.mapType = .standard
options.pointOfInterestFilter = .excludingAll
options.showsBuildings = true
options.size = CGSize(width: CGFloat(resource.width + 1), height: CGFloat(resource.height + 10))
options.scale = 2.0
let snapshotter = MKMapSnapshotter(options: options)
snapshotter.start(with: DispatchQueue.global(), completionHandler: { result, error in
if let image = result?.image {
if let data = image.jpegData(compressionQuality: 0.9) {
let tempFile = EngineTempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) {
subscriber.putNext(.moveTempFile(file: tempFile))
subscriber.putCompletion()
}
}
}
})
disposable.set(ActionDisposable {
snapshotter.cancel()
})
}
return disposable
}
}
public func chatMapSnapshotData(engine: TelegramEngine, resource: MapSnapshotMediaResource) -> Signal<Data?, NoError> {
return Signal<Data?, NoError> { subscriber in
let dataDisposable = engine.resources.custom(
id: resource.id.stringRepresentation,
fetch: EngineMediaResource.Fetch {
return fetchMapSnapshotResource(resource: resource)
},
cacheTimeout: .shortLived
).start(next: { next in
if next.availableSize != 0 {
subscriber.putNext(next.availableSize == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))
}
}, error: subscriber.putError, completed: subscriber.putCompletion)
return ActionDisposable {
dataDisposable.dispose()
}
}
}
public func chatMapSnapshotImage(engine: TelegramEngine, resource: MapSnapshotMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let signal = chatMapSnapshotData(engine: engine, resource: resource)
return signal |> map { fullSizeData in
return { arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
var fullSizeImage: CGImage?
if let fullSizeData = fullSizeData {
let options = NSMutableDictionary()
options[kCGImageSourceShouldCache as NSString] = false as NSNumber
if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) {
fullSizeImage = image
}
if let fullSizeImage = fullSizeImage {
let drawingRect = arguments.drawingRect
var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size)
if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) {
fittedSize.width = arguments.boundingSize.width
}
if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) {
fittedSize.height = arguments.boundingSize.height
}
let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)
context.withFlippedContext { c in
c.setBlendMode(.copy)
if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height {
c.fill(arguments.drawingRect)
}
c.setBlendMode(.copy)
c.interpolationQuality = .medium
c.draw(fullSizeImage, in: fittedRect)
c.setBlendMode(.normal)
}
} else {
context.withFlippedContext { c in
c.setBlendMode(.copy)
c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor)
c.fill(arguments.drawingRect)
c.setBlendMode(.normal)
}
}
}
addCorners(context, arguments: arguments)
return context
}
}
}
@@ -0,0 +1,203 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AppBundle
import PersistentStringHash
public struct VenueIconResourceId {
public let type: String
public init(type: String) {
self.type = type
}
public var uniqueId: String {
return "venue-icon-\(self.type.replacingOccurrences(of: "/", with: "_"))"
}
public var hashValue: Int {
return self.type.hashValue
}
}
public class VenueIconResource {
public let type: String
public init(type: String) {
self.type = type
}
public var id: EngineMediaResource.Id {
return EngineMediaResource.Id(VenueIconResourceId(type: self.type).uniqueId)
}
}
private func fetchVenueIconResource(engine: TelegramEngine, resource: VenueIconResource) -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> {
return Signal { subscriber in
let url = "https://ss3.4sqi.net/img/categories_v2/\(resource.type)_88.png"
return engine.resources.httpData(url: url).start(next: { data in
let file = EngineTempBox.shared.tempFile(fileName: "file.png")
let _ = try? data.write(to: URL(fileURLWithPath: file.path))
subscriber.putNext(.moveTempFile(file: file))
}, completed: {
subscriber.putCompletion()
})
}
}
private func venueIconData(engine: TelegramEngine, resource: VenueIconResource) -> Signal<Data?, NoError> {
let resourceData = engine.resources.custom(
id: resource.id.stringRepresentation,
fetch: EngineMediaResource.Fetch {
return fetchVenueIconResource(engine: engine, resource: resource)
},
cacheTimeout: .shortLived
)
let signal = resourceData
|> take(1)
|> mapToSignal { maybeData -> Signal<Data?, NoError> in
if maybeData.isComplete {
return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)))
} else {
return .single(nil)
}
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs == nil && rhs == nil {
return true
} else {
return false
}
})
return signal
}
private let randomColors = [UIColor(rgb: 0xe56cd5), UIColor(rgb: 0xf89440), UIColor(rgb: 0x9986ff), UIColor(rgb: 0x44b3f5), UIColor(rgb: 0x6dc139), UIColor(rgb: 0xff5d5a), UIColor(rgb: 0xf87aad), UIColor(rgb: 0x6e82b3), UIColor(rgb: 0xf5ba21)]
private let venueColors: [String: UIColor] = [
"building/medical": UIColor(rgb: 0x43b3f4), // light blue?
"building/gym": UIColor(rgb: 0x43b3f4), // light blue?
"arts_entertainment": UIColor(rgb: 0xaf52de), // purple
"travel/bedandbreakfast": UIColor(rgb: 0x9987ff),
"travel/hotel": UIColor(rgb: 0x9987ff),
"travel/hostel": UIColor(rgb: 0x9987ff),
"travel/resort": UIColor(rgb: 0x9987ff),
"building": UIColor(rgb: 0x6e81b2),
"education": UIColor(rgb: 0xa57348),
"event": UIColor(rgb: 0x959595),
"food": UIColor(rgb: 0xff9500), // orange
"education/cafeteria": UIColor(rgb: 0xff9500), // orange
"nightlife": UIColor(rgb: 0xaf52de), // purple
"travel/hotel_bar": UIColor(rgb: 0xaf52de), // purple
"parks_outdoors": UIColor(rgb: 0x6cc138), // green
"shops": UIColor(rgb: 0xffb300),
"travel": UIColor(rgb: 0x1c9fff),
"work": UIColor(rgb: 0xad7854),
"home": UIColor(rgb: 0x00aeef)
]
public func venueIconColor(type: String) -> UIColor {
if type.isEmpty {
return UIColor(rgb: 0x008df2)
}
if let color = venueColors[type] {
return color
}
let generalType = type.components(separatedBy: "/").first ?? type
if let color = venueColors[generalType] {
return color
}
let index = Int(abs(Int32(bitPattern: UInt32(clamping: type.persistentHashValue))) % Int32(randomColors.count))
return randomColors[index]
}
public struct VenueIconArguments: TransformImageCustomArguments {
let defaultBackgroundColor: UIColor
let defaultForegroundColor: UIColor
public init(defaultBackgroundColor: UIColor, defaultForegroundColor: UIColor) {
self.defaultBackgroundColor = defaultBackgroundColor
self.defaultForegroundColor = defaultForegroundColor
}
public func serialized() -> NSArray {
let array = NSMutableArray()
array.add(self.defaultBackgroundColor)
array.add(self.defaultForegroundColor)
return array
}
}
public func venueIcon(engine: TelegramEngine, type: String, flag: String? = nil, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let isBuiltinIcon = ["", "home", "work"].contains(type) || flag != nil
let data: Signal<Data?, NoError> = isBuiltinIcon ? .single(nil) : venueIconData(engine: engine, resource: VenueIconResource(type: type))
return data |> map { data in
return { arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
var iconImage: UIImage?
if let data = data, let image = UIImage(data: data) {
iconImage = image
}
let backgroundColor: UIColor
let foregroundColor: UIColor
if type.isEmpty || type == "building/default", let customArguments = arguments.custom as? VenueIconArguments {
backgroundColor = customArguments.defaultBackgroundColor
foregroundColor = customArguments.defaultForegroundColor
} else {
backgroundColor = venueIconColor(type: type)
foregroundColor = UIColor.white
}
context.withFlippedContext { c in
if background {
c.setFillColor(backgroundColor.cgColor)
c.fillEllipse(in: CGRect(origin: CGPoint(), size: arguments.drawingRect.size))
}
let boundsSize = CGSize(width: arguments.drawingRect.size.width - 4.0 * 2.0, height: arguments.drawingRect.size.height - 4.0 * 2.0)
if let flag {
let attributedString = NSAttributedString(string: flag, attributes: [NSAttributedString.Key.font: Font.regular(22.0), NSAttributedString.Key.foregroundColor: UIColor.white])
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
let bounds = CGRect(origin: .zero, size: boundsSize)
let lineOrigin = CGPoint(x: floorToScreenPixels((bounds.size.width - lineBounds.size.width) / 2.0), y: floorToScreenPixels((bounds.size.height - lineBounds.size.height) / 2.0))
c.translateBy(x: lineOrigin.x + 3.0, y: lineOrigin.y + 7.0)
CTLineDraw(line, c)
} else if let image = iconImage, let cgImage = generateTintedImage(image: image, color: foregroundColor)?.cgImage {
let fittedSize = image.size.aspectFitted(boundsSize)
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize))
} else if isBuiltinIcon {
let image: UIImage?
switch type {
case "":
image = UIImage(bundleImageName: "Chat/Message/LocationPinForeground")
case "home":
image = UIImage(bundleImageName: "Location/HomeIcon")
case "work":
image = UIImage(bundleImageName: "Location/WorkIcon")
default:
image = nil
}
if let image = image, let pinImage = generateTintedImage(image: image, color: foregroundColor), let cgImage = pinImage.cgImage {
let fittedSize = image.size.aspectFitted(boundsSize)
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize))
}
}
}
return context
}
}
}