mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
b4cc4e0bd0
commit
49e5ac7e59
@ -113,12 +113,7 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
|
||||
var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude))
|
||||
self.textSize = result
|
||||
|
||||
let widthExtension: CGFloat
|
||||
if self.weatherEntity.icon != nil {
|
||||
widthExtension = result.height * 0.77
|
||||
} else {
|
||||
widthExtension = result.height * 0.65
|
||||
}
|
||||
let widthExtension: CGFloat = result.height * 0.7
|
||||
result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension)
|
||||
result.height = ceil(result.height * 1.2);
|
||||
return result;
|
||||
@ -136,19 +131,17 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let iconSize: CGFloat
|
||||
let iconOffset: CGFloat
|
||||
if self.weatherEntity.icon != nil {
|
||||
iconSize = min(80.0, floor(self.bounds.height * 0.7))
|
||||
iconOffset = 0.2
|
||||
} else {
|
||||
iconSize = min(76.0, floor(self.bounds.height * 0.6))
|
||||
iconOffset = 0.3
|
||||
}
|
||||
|
||||
let iconSize = min(80.0, floor(self.bounds.height * 0.7))
|
||||
let iconOffset: CGFloat = 0.3
|
||||
|
||||
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
|
||||
self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
|
||||
animationNode.updateLayout(size: self.iconView.frame.size)
|
||||
}
|
||||
|
||||
let imageSize = CGSize(width: iconSize, height: iconSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
|
||||
@ -304,29 +297,19 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
|
||||
self.addSubnode(self.imageNode)
|
||||
if let dimensions = file.dimensions {
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))
|
||||
if self.animationNode == nil {
|
||||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||||
animationNode.autoplay = false
|
||||
animationNode.autoplay = true
|
||||
self.animationNode = animationNode
|
||||
animationNode.started = { [weak self, weak animationNode] in
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
|
||||
let _ = animationNode
|
||||
// if let animationNode = animationNode {
|
||||
// let _ = (animationNode.status
|
||||
// |> take(1)
|
||||
// |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
// self?.started?(status.duration)
|
||||
// })
|
||||
// }
|
||||
}
|
||||
self.addSubnode(animationNode)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
|
||||
if file.isCustomTemplateEmoji {
|
||||
animationNode.dynamicColor = UIColor(rgb: 0xffffff)
|
||||
}
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
|
@ -28,6 +28,10 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage,
|
||||
angle = -entity.rotation
|
||||
scale = entity.scale
|
||||
position = entity.position
|
||||
} else if let entity = entity as? DrawingWeatherEntity {
|
||||
angle = -entity.rotation
|
||||
scale = entity.scale
|
||||
position = entity.position
|
||||
} else if let entity = entity as? DrawingLinkEntity {
|
||||
angle = -entity.rotation
|
||||
scale = entity.scale
|
||||
@ -122,6 +126,8 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti
|
||||
return entities
|
||||
} else if let entity = entity as? DrawingLocationEntity {
|
||||
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
|
||||
} else if let entity = entity as? DrawingWeatherEntity {
|
||||
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
|
||||
} else if let entity = entity as? DrawingLinkEntity {
|
||||
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/WebsiteType",
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/DeviceLocationManager",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -2545,6 +2545,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
private(set) var hasAnyChanges = false
|
||||
|
||||
fileprivate var drawingScreen: DrawingScreen?
|
||||
fileprivate var stickerScreen: StickerPickerScreen?
|
||||
fileprivate weak var cutoutScreen: MediaCutoutScreen?
|
||||
private var defaultToEmoji = false
|
||||
|
||||
private var previousDrawingData: Data?
|
||||
private var previousDrawingEntities: [DrawingEntity]?
|
||||
|
||||
private var weatherPromise: Promise<StickerPickerScreen.Weather>?
|
||||
|
||||
private var playbackPositionDisposable: Disposable?
|
||||
|
||||
var recording: MediaEditorScreen.Recording
|
||||
@ -4572,61 +4582,21 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.mediaEditor?.play()
|
||||
}
|
||||
|
||||
func addWeather() {
|
||||
if !self.didSetupStaticEmojiPack {
|
||||
self.didSetupStaticEmojiPack = true
|
||||
self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false))
|
||||
}
|
||||
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) {
|
||||
let weatherFormatter = MeasurementFormatter()
|
||||
weatherFormatter.locale = Locale.current
|
||||
weatherFormatter.unitStyle = .short
|
||||
weatherFormatter.numberFormatter.maximumFractionDigits = 0
|
||||
|
||||
let emojiFile: Signal<TelegramMediaFile?, NoError>
|
||||
let emoji = "☀️".strippedEmoji
|
||||
|
||||
emojiFile = self.context.animatedEmojiStickers
|
||||
|> take(1)
|
||||
|> map { result -> TelegramMediaFile? in
|
||||
if let file = result[emoji]?.first {
|
||||
return file.file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if case let .result(_, items, _) = result, let match = items.first(where: { item in
|
||||
// var displayText: String?
|
||||
// for attribute in item.file.attributes {
|
||||
// if case let .Sticker(alt, _, _) = attribute {
|
||||
// displayText = alt
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if let displayText, displayText.hasPrefix(emoji) {
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }) {
|
||||
// return match.file
|
||||
// } else {
|
||||
// return nil
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
let _ = (emojiFile
|
||||
|> deliverOnMainQueue).start(next: { [weak self] emojiFile in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let scale = 1.0
|
||||
self.interaction?.insertEntity(
|
||||
DrawingWeatherEntity(
|
||||
temperature: "35°C",
|
||||
style: .white,
|
||||
icon: emojiFile
|
||||
),
|
||||
scale: scale,
|
||||
position: nil
|
||||
)
|
||||
})
|
||||
self.interaction?.insertEntity(
|
||||
DrawingWeatherEntity(
|
||||
temperature: weatherFormatter.string(from: Measurement(value: weather.temperature, unit: UnitTemperature.celsius)),
|
||||
style: .white,
|
||||
icon: weather.emojiFile
|
||||
),
|
||||
scale: nil,
|
||||
position: nil
|
||||
)
|
||||
}
|
||||
|
||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@ -4707,15 +4677,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return self.previewContentContainerView
|
||||
}
|
||||
|
||||
fileprivate var drawingScreen: DrawingScreen?
|
||||
fileprivate var stickerScreen: StickerPickerScreen?
|
||||
fileprivate weak var cutoutScreen: MediaCutoutScreen?
|
||||
private var defaultToEmoji = false
|
||||
|
||||
private var previousDrawingData: Data?
|
||||
private var previousDrawingEntities: [DrawingEntity]?
|
||||
|
||||
|
||||
func requestLayout(forceUpdate: Bool, transition: ComponentTransition) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
@ -4812,7 +4774,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let controller = self.controller, case .stickerEditor = controller.mode {
|
||||
hasInteractiveStickers = false
|
||||
}
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers)
|
||||
|
||||
var weatherSignal: Signal<StickerPickerScreen.Weather, NoError>
|
||||
if "".isEmpty {
|
||||
let weatherPromise: Promise<StickerPickerScreen.Weather>
|
||||
if let current = self.weatherPromise {
|
||||
weatherPromise = current
|
||||
} else {
|
||||
weatherPromise = Promise()
|
||||
weatherPromise.set(getWeather(context: self.context))
|
||||
self.weatherPromise = weatherPromise
|
||||
}
|
||||
weatherSignal = weatherPromise.get()
|
||||
} else {
|
||||
weatherSignal = .single(.none)
|
||||
}
|
||||
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers, weather: weatherSignal)
|
||||
controller.completion = { [weak self] content in
|
||||
guard let self else {
|
||||
return false
|
||||
@ -4885,7 +4863,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
controller.addWeather = { [weak self, weak controller] in
|
||||
if let self {
|
||||
self.addWeather()
|
||||
if let weatherPromise = self.weatherPromise {
|
||||
let _ = (weatherPromise.get()
|
||||
|> take(1)).start(next: { [weak self] weather in
|
||||
if let self, case let .loaded(loaded) = weather {
|
||||
self.addWeather(loaded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.stickerScreen = nil
|
||||
controller?.dismiss(animated: true)
|
||||
|
@ -1,16 +1,21 @@
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import StickerPickerScreen
|
||||
import AccountContext
|
||||
import DeviceLocationManager
|
||||
|
||||
func emojiFor(for meteocode: Int, timestamp: Int32) -> String {
|
||||
func emojiFor(for meteocode: Int, date: Date, location: CLLocationCoordinate2D) -> String? {
|
||||
var emoji = weatherEmoji(for: meteocode)
|
||||
if ["☀️", "🌤️"].contains(emoji) {
|
||||
emoji = moonPhaseEmoji(for: timestamp)
|
||||
if ["☀️", "🌤️"].contains(emoji) && isNightTime(date: date, location: location) {
|
||||
emoji = moonPhaseEmoji(for: date)
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
|
||||
func moonPhaseEmoji(for timestamp: Int32) -> String {
|
||||
private func moonPhaseEmoji(for date: Date) -> String {
|
||||
let newMoonDate = Date(timeIntervalSince1970: 1612137600)
|
||||
let date = Date(timeIntervalSince1970: Double(timestamp))
|
||||
let lunarMonth: TimeInterval = 29.53058867 * 24 * 60 * 60
|
||||
|
||||
let daysSinceNewMoon = date.timeIntervalSince(newMoonDate) / (24 * 60 * 60)
|
||||
@ -38,7 +43,7 @@ func moonPhaseEmoji(for timestamp: Int32) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
func weatherEmoji(for meteocode: Int) -> String {
|
||||
func weatherEmoji(for meteocode: Int) -> String? {
|
||||
switch meteocode {
|
||||
case 0:
|
||||
return "☀️"
|
||||
@ -65,6 +70,188 @@ func weatherEmoji(for meteocode: Int) -> String {
|
||||
case 95, 96, 99:
|
||||
return "⛈️" // Thunderstorm: Slight or moderate
|
||||
default:
|
||||
return "❓"
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct StoryWeather {
|
||||
let emoji: String
|
||||
let temperature: Double
|
||||
}
|
||||
|
||||
private func getWeatherData(location: CLLocationCoordinate2D) -> Signal<StoryWeather?, NoError> {
|
||||
let latitude = "\(location.latitude)"
|
||||
let longitude = "\(location.longitude)"
|
||||
let url = "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)¤t=temperature_2m,weather_code"
|
||||
|
||||
return Signal { subscriber in
|
||||
let disposable = fetchHttpResource(url: url).start(next: { result in
|
||||
if case let .dataPart(_, data, _, complete) = result, complete {
|
||||
guard let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
|
||||
subscriber.putNext(nil)
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
guard let current = dict["current"] as? [String: Any], let temperature = current["temperature_2m"] as? Double, let weatherCode = current["weather_code"] as? Int else {
|
||||
subscriber.putNext(nil)
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
if let emoji = emojiFor(for: weatherCode, date: Date(), location: location) {
|
||||
subscriber.putNext(StoryWeather(emoji: emoji, temperature: temperature))
|
||||
} else {
|
||||
subscriber.putNext(nil)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
func getWeather(context: AccountContext) -> Signal<StickerPickerScreen.Weather, NoError> {
|
||||
guard let locationManager = context.sharedContext.locationManager else {
|
||||
return .single(.none)
|
||||
}
|
||||
return .single(.fetching)
|
||||
|> then(
|
||||
currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|
||||
|> mapToSignal { location in
|
||||
if let location {
|
||||
return getWeatherData(location: location)
|
||||
|> mapToSignal { weather in
|
||||
if let weather {
|
||||
return context.animatedEmojiStickers
|
||||
|> take(1)
|
||||
|> mapToSignal { result in
|
||||
if let match = result[weather.emoji.strippedEmoji]?.first {
|
||||
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
|
||||
emoji: weather.emoji.strippedEmoji,
|
||||
emojiFile: match.file,
|
||||
temperature: weather.temperature
|
||||
)))
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func calculateSunriseSunset(date: Date, location: CLLocationCoordinate2D) -> (sunrise: Date, sunset: Date)? {
|
||||
guard let utcTimezone = TimeZone(identifier: "UTC") else { return nil }
|
||||
|
||||
let zenith: Double = 90.83
|
||||
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = utcTimezone
|
||||
|
||||
guard let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
func toRadians(_ degrees: Double) -> Double {
|
||||
return degrees * .pi / 180.0
|
||||
}
|
||||
|
||||
func toDegrees(_ radians: Double) -> Double {
|
||||
return radians * 180.0 / .pi
|
||||
}
|
||||
|
||||
func normalise(_ value: Double, maximum: Double) -> Double {
|
||||
var value = value
|
||||
if value < 0 {
|
||||
value += maximum
|
||||
}
|
||||
if value > maximum {
|
||||
value -= maximum
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func calculateTime(isSunrise: Bool) -> Date? {
|
||||
let day = Double(dayOfYear)
|
||||
let lngHour = location.longitude / 15.0
|
||||
|
||||
let hourTime: Double = isSunrise ? 6 : 18
|
||||
let t = day + ((hourTime - lngHour) / 24)
|
||||
|
||||
let M = (0.9856 * t) - 3.289
|
||||
|
||||
var L = M + 1.916 * sin(toRadians(M)) + 0.020 * sin(2 * toRadians(M)) + 282.634
|
||||
L = normalise(L, maximum: 360)
|
||||
|
||||
var RA = toDegrees(atan(0.91764 * tan(toRadians(L))))
|
||||
RA = normalise(RA, maximum: 360)
|
||||
|
||||
let Lquadrant = floor(L / 90) * 90
|
||||
let RAquadrant = floor(RA / 90) * 90
|
||||
RA = RA + (Lquadrant - RAquadrant)
|
||||
RA = RA / 15
|
||||
|
||||
let sinDec = 0.39782 * sin(toRadians(L))
|
||||
let cosDec = cos(asin(sinDec))
|
||||
let cosH = (cos(toRadians(zenith)) - (sinDec * sin(toRadians(location.latitude)))) / (cosDec * cos(toRadians(location.latitude)))
|
||||
guard cosH < 1 else {
|
||||
return nil
|
||||
}
|
||||
guard cosH > -1 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let tempH = isSunrise ? 360.0 - toDegrees(acos(cosH)) : toDegrees(acos(cosH))
|
||||
let H = tempH / 15.0
|
||||
let T = H + RA - (0.06571 * t) - 6.622
|
||||
|
||||
var UT = T - lngHour
|
||||
UT = normalise(UT, maximum: 24)
|
||||
|
||||
let hour = floor(UT)
|
||||
let minute = floor((UT - hour) * 60.0)
|
||||
let second = (((UT - hour) * 60) - minute) * 60.0
|
||||
|
||||
let shouldBeYesterday = lngHour > 0 && UT > 12 && isSunrise
|
||||
let shouldBeTomorrow = lngHour < 0 && UT < 12 && !isSunrise
|
||||
|
||||
let setDate: Date
|
||||
if shouldBeYesterday {
|
||||
setDate = Date(timeInterval: -(60 * 60 * 24), since: date)
|
||||
} else if shouldBeTomorrow {
|
||||
setDate = Date(timeInterval: (60 * 60 * 24), since: date)
|
||||
} else {
|
||||
setDate = date
|
||||
}
|
||||
|
||||
var components = calendar.dateComponents([.day, .month, .year], from: setDate)
|
||||
components.hour = Int(hour)
|
||||
components.minute = Int(minute)
|
||||
components.second = Int(second)
|
||||
|
||||
calendar.timeZone = utcTimezone
|
||||
return calendar.date(from: components)
|
||||
}
|
||||
|
||||
guard let sunrise = calculateTime(isSunrise: true),
|
||||
let sunset = calculateTime(isSunrise: false) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (sunrise, sunset)
|
||||
}
|
||||
|
||||
private func isNightTime(date: Date, location: CLLocationCoordinate2D) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
let date = calendar.startOfDay(for: date)
|
||||
guard let (sunrise, sunset) = calculateSunriseSunset(date: date, location: location) else {
|
||||
return false
|
||||
}
|
||||
return date < sunrise || date > sunset
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
|
@ -20,6 +20,8 @@ import MediaEditor
|
||||
import EntityKeyboardGifContent
|
||||
import CameraButtonComponent
|
||||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
import LottieComponentResourceContent
|
||||
import UndoUI
|
||||
import GalleryUI
|
||||
|
||||
@ -532,7 +534,10 @@ public class StickerPickerScreen: ViewController {
|
||||
self.containerView.addSubview(self.hostView)
|
||||
|
||||
if controller.hasInteractiveStickers {
|
||||
self.storyStickersContentView = StoryStickersContentView(isPremium: context.isPremium)
|
||||
self.storyStickersContentView = StoryStickersContentView(
|
||||
context: context,
|
||||
weather: controller.weather
|
||||
)
|
||||
self.storyStickersContentView?.locationAction = { [weak self] in
|
||||
self?.controller?.presentLocationPicker()
|
||||
}
|
||||
@ -2044,15 +2049,34 @@ public class StickerPickerScreen: ViewController {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
public enum Weather {
|
||||
public struct LoadedWeather {
|
||||
public let emoji: String
|
||||
public let emojiFile: TelegramMediaFile
|
||||
public let temperature: Double
|
||||
|
||||
public init(emoji: String, emojiFile: TelegramMediaFile, temperature: Double) {
|
||||
self.emoji = emoji
|
||||
self.emojiFile = emojiFile
|
||||
self.temperature = temperature
|
||||
}
|
||||
}
|
||||
|
||||
case none
|
||||
case fetching
|
||||
case loaded(StickerPickerScreen.Weather.LoadedWeather)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme
|
||||
fileprivate let forceDark: Bool
|
||||
let forceDark: Bool
|
||||
private let inputData: Signal<StickerPickerInput, NoError>
|
||||
fileprivate let defaultToEmoji: Bool
|
||||
let defaultToEmoji: Bool
|
||||
let isFullscreen: Bool
|
||||
let hasEmoji: Bool
|
||||
let hasGifs: Bool
|
||||
let hasInteractiveStickers: Bool
|
||||
let weather: Signal<StickerPickerScreen.Weather, NoError>
|
||||
|
||||
private var currentLayout: ContainerViewLayout?
|
||||
|
||||
@ -2068,7 +2092,7 @@ public class StickerPickerScreen: ViewController {
|
||||
public var addLink: () -> Void = { }
|
||||
public var addWeather: () -> Void = { }
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInput, NoError>, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) {
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInput, NoError>, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true, weather: Signal<StickerPickerScreen.Weather, NoError> = .single(.none)) {
|
||||
self.context = context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme
|
||||
@ -2079,6 +2103,7 @@ public class StickerPickerScreen: ViewController {
|
||||
self.hasEmoji = hasEmoji
|
||||
self.hasGifs = hasGifs
|
||||
self.hasInteractiveStickers = hasInteractiveStickers
|
||||
self.weather = weather
|
||||
|
||||
super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil)
|
||||
|
||||
@ -2141,22 +2166,28 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
|
||||
private final class InteractiveStickerButtonContent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let iconName: String
|
||||
let title: String?
|
||||
let iconName: String?
|
||||
let iconFile: TelegramMediaFile?
|
||||
let useOpaqueTheme: Bool
|
||||
weak var tintContainerView: UIView?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
title: String,
|
||||
iconName: String,
|
||||
title: String?,
|
||||
iconName: String?,
|
||||
iconFile: TelegramMediaFile? = nil,
|
||||
useOpaqueTheme: Bool,
|
||||
tintContainerView: UIView
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.iconName = iconName
|
||||
self.iconFile = iconFile
|
||||
self.useOpaqueTheme = useOpaqueTheme
|
||||
self.tintContainerView = tintContainerView
|
||||
}
|
||||
@ -2207,68 +2238,80 @@ private final class InteractiveStickerButtonContent: Component {
|
||||
|
||||
func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
|
||||
|
||||
|
||||
let iconSize: CGSize
|
||||
if component.iconName == "Sun" {
|
||||
iconSize = self.icon.update(
|
||||
let buttonSize: CGSize
|
||||
if let title = component.title {
|
||||
if let iconFile = component.iconFile {
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.ResourceContent(context: component.context, file: iconFile, attemptSynchronously: true, providesPlaceholder: true),
|
||||
color: nil,
|
||||
placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4),
|
||||
loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(component.iconName ?? "")
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||
)
|
||||
} else {
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: component.iconName ?? "",
|
||||
tintColor: .white,
|
||||
maxSize: CGSize(width: 20.0, height: 20.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
}
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: "☀️",
|
||||
text: title.uppercased(),
|
||||
font: Font.with(size: 23.0, design: .camera),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let padding: CGFloat = 7.0
|
||||
let spacing: CGFloat = 4.0
|
||||
buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0)
|
||||
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
if let view = self.title.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize))
|
||||
}
|
||||
} else {
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: component.iconName,
|
||||
tintColor: .white,
|
||||
maxSize: CGSize(width: 20.0, height: 20.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
}
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.title.uppercased(),
|
||||
font: Font.with(size: 23.0, design: .camera),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let padding: CGFloat = 7.0
|
||||
let spacing: CGFloat = 4.0
|
||||
let buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0)
|
||||
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
if let view = self.title.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize))
|
||||
buttonSize = CGSize(width: 87.0, height: 34.0)
|
||||
}
|
||||
|
||||
self.backgroundLayer.cornerRadius = 6.0
|
||||
self.tintBackgroundLayer.cornerRadius = 6.0
|
||||
|
||||
self.backgroundLayer.frame = CGRect(origin: .zero, size: buttonSize)
|
||||
var transition = transition
|
||||
if self.backgroundLayer.frame.width.isZero {
|
||||
transition = .immediate
|
||||
}
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: buttonSize))
|
||||
|
||||
if self.tintBackgroundLayer.superlayer == nil, let tintContainerView = component.tintContainerView {
|
||||
Queue.mainQueue().justDispatch {
|
||||
let mappedFrame = self.convert(self.bounds, to: tintContainerView)
|
||||
self.tintBackgroundLayer.frame = mappedFrame
|
||||
transition.setFrame(layer: self.tintBackgroundLayer, frame: mappedFrame)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2441,12 +2484,14 @@ final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
private let padding: CGFloat
|
||||
private let minSpacing: CGFloat
|
||||
private let verticalSpacing: CGFloat
|
||||
private let maxHorizontalItems: Int
|
||||
|
||||
init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat) {
|
||||
init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat, maxHorizontalItems: Int) {
|
||||
self.items = items
|
||||
self.padding = padding
|
||||
self.minSpacing = minSpacing
|
||||
self.verticalSpacing = verticalSpacing
|
||||
self.maxHorizontalItems = maxHorizontalItems
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemStack<ChildEnvironment>, rhs: ItemStack<ChildEnvironment>) -> Bool {
|
||||
@ -2462,6 +2507,9 @@ final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
if lhs.verticalSpacing != rhs.verticalSpacing {
|
||||
return false
|
||||
}
|
||||
if lhs.maxHorizontalItems != rhs.maxHorizontalItems {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2491,7 +2539,7 @@ final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
|
||||
let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0
|
||||
let spacing = remainingWidth / CGFloat(rowItemsCount - 1)
|
||||
if spacing < context.component.minSpacing || currentGroup.count == 3 {
|
||||
if spacing < context.component.minSpacing || currentGroup.count == context.component.maxHorizontalItems {
|
||||
groups.append(currentGroup)
|
||||
currentGroup = []
|
||||
}
|
||||
@ -2545,148 +2593,233 @@ final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
}
|
||||
|
||||
final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
||||
private let context: AccountContext
|
||||
private let weatherFormatter: MeasurementFormatter
|
||||
|
||||
let tintContainerView = UIView()
|
||||
|
||||
private let container = ComponentView<Empty>()
|
||||
|
||||
private let isPremium: Bool
|
||||
private var weatherDisposable: Disposable?
|
||||
private var weather: StickerPickerScreen.Weather = .none
|
||||
|
||||
var locationAction: () -> Void = {}
|
||||
var audioAction: () -> Void = {}
|
||||
var reactionAction: () -> Void = {}
|
||||
var linkAction: () -> Void = {}
|
||||
var weatherAction: () -> Void = {}
|
||||
|
||||
private var params: (theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize)?
|
||||
|
||||
init(isPremium: Bool) {
|
||||
self.isPremium = isPremium
|
||||
init(context: AccountContext, weather: Signal<StickerPickerScreen.Weather, NoError>) {
|
||||
self.context = context
|
||||
self.weatherFormatter = MeasurementFormatter()
|
||||
self.weatherFormatter.locale = Locale.current
|
||||
self.weatherFormatter.unitStyle = .short
|
||||
self.weatherFormatter.numberFormatter.maximumFractionDigits = 0
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.weatherDisposable = (weather
|
||||
|> deliverOnMainQueue).start(next: { [weak self] weather in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.weather = weather
|
||||
if let (theme, strings, useOpaqueTheme, availableSize) = self.params {
|
||||
let _ = self.update(theme: theme, strings: strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableSize, transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.weatherDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
self.params = (theme, strings, useOpaqueTheme, availableSize)
|
||||
|
||||
let padding: CGFloat = 22.0
|
||||
var maxHorizontalItems = 2
|
||||
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "link",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "content",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddLink,
|
||||
iconName: self.context.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.linkAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "location",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "content",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddLocationShort,
|
||||
iconName: "Chat/Attach Menu/Location",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.locationAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if case .none = self.weather {
|
||||
|
||||
} else {
|
||||
maxHorizontalItems = 3
|
||||
|
||||
switch self.weather {
|
||||
case let .loaded(weather):
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: self.weatherFormatter.string(from: Measurement(value: weather.temperature, unit: UnitTemperature.celsius)),
|
||||
iconName: weather.emoji,
|
||||
iconFile: weather.emojiFile,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
case .fetching:
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: nil,
|
||||
iconName: nil,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "audio",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "audio",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddAudio,
|
||||
iconName: "Media Editor/Audio",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.audioAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "reaction",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "reaction",
|
||||
component: AnyComponent(
|
||||
InteractiveReactionButtonContent(theme: theme)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.reactionAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let size = self.container.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
ItemStack(
|
||||
[
|
||||
AnyComponentWithIdentity(
|
||||
id: "link",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "content",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddLink,
|
||||
iconName: self.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.linkAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "location",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "content",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddLocationShort,
|
||||
iconName: "Chat/Attach Menu/Location",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.locationAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
theme: theme,
|
||||
title: "35°C",
|
||||
iconName: "Sun",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "audio",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "audio",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
theme: theme,
|
||||
title: strings.MediaEditor_AddAudio,
|
||||
iconName: "Media Editor/Audio",
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.audioAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "reaction",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "reaction",
|
||||
component: AnyComponent(
|
||||
InteractiveReactionButtonContent(theme: theme)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.reactionAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
],
|
||||
items,
|
||||
padding: 18.0,
|
||||
minSpacing: 8.0,
|
||||
verticalSpacing: 12.0
|
||||
verticalSpacing: 12.0,
|
||||
maxHorizontalItems: maxHorizontalItems
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
|
Loading…
x
Reference in New Issue
Block a user