Various improvements

This commit is contained in:
Ilya Laktyushin 2024-07-16 05:57:44 +04:00
parent b4cc4e0bd0
commit 49e5ac7e59
7 changed files with 570 additions and 273 deletions

View File

@ -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 {

View File

@ -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)]
}

View File

@ -63,6 +63,7 @@ swift_library(
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/WebsiteType",
"//submodules/UrlEscaping",
"//submodules/DeviceLocationManager",
],
visibility = [
"//visibility:public",

View File

@ -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)

View File

@ -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)&current=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
}

View File

@ -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",

View File

@ -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: {},