Live location fixes

This commit is contained in:
Ilya Laktyushin 2020-10-23 19:11:32 +04:00
parent 27c6b78465
commit 9e2447d78b
14 changed files with 347 additions and 181 deletions

View File

@ -158,7 +158,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
let addedStopped = stopMessageIds.subtracting(self.stopMessageIds) let addedStopped = stopMessageIds.subtracting(self.stopMessageIds)
self.stopMessageIds = stopMessageIds self.stopMessageIds = stopMessageIds
for id in addedStopped { for id in addedStopped {
self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, coordinate: nil, heading: nil) self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: true, coordinate: nil, heading: nil, proximityNotificationRadius: nil)
|> deliverOn(self.queue)).start(completed: { [weak self] in |> deliverOn(self.queue)).start(completed: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.editMessageDisposables.set(nil, forKey: id) strongSelf.editMessageDisposables.set(nil, forKey: id)
@ -213,7 +213,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
let ids = self.broadcastToMessageIds let ids = self.broadcastToMessageIds
let remainingIds = Atomic<Set<MessageId>>(value: Set(ids.keys)) let remainingIds = Atomic<Set<MessageId>>(value: Set(ids.keys))
for id in ids.keys { for id in ids.keys {
self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: Int32(heading ?? 0)) self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: Int32(heading ?? 0), proximityNotificationRadius: nil)
|> deliverOn(self.queue)).start(completed: { [weak self] in |> deliverOn(self.queue)).start(completed: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.editMessageDisposables.set(nil, forKey: id) strongSelf.editMessageDisposables.set(nil, forKey: id)

View File

@ -375,8 +375,12 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.updateDoneButtonTitle() if pickerView.selectedRow(inComponent: 0) == 0 && pickerView.selectedRow(inComponent: 1) == 0 {
self.update() pickerView.selectRow(1, inComponent: 1, animated: true)
} else {
self.updateDoneButtonTitle()
self.update()
}
} }
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {

View File

@ -154,9 +154,9 @@ final class LocationMapHeaderNode: ASDisplayNode {
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected) self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted]) self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/NotificationIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected) self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted]) self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
} }

View File

@ -133,7 +133,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
var radius: Double var radius: Double
var alpha: CGFloat { var alpha: CGFloat {
didSet { didSet {
self.alphaTransition = (oldValue, CACurrentMediaTime(), 0.5) self.alphaTransition = (oldValue, CACurrentMediaTime(), 0.3)
} }
} }
var alphaTransition: (from: CGFloat, startTimestamp: Double, duration: Double)? var alphaTransition: (from: CGFloat, startTimestamp: Double, duration: Double)?
@ -151,7 +151,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
class InvertedProximityCircleRenderer: MKOverlayRenderer { class InvertedProximityCircleRenderer: MKOverlayRenderer {
var radius: Double = 0.0 var radius: Double = 0.0
var fillColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.4) var fillColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.5)
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = self.overlay as? InvertedProximityCircle else { guard let overlay = self.overlay as? InvertedProximityCircle else {

View File

@ -7,8 +7,8 @@ import TelegramStringFormatting
import MapKit import MapKit
extension TelegramMediaMap { extension TelegramMediaMap {
convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil) { convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil, proximityNotificationRadius: Int32? = nil) {
self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: nil) self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: proximityNotificationRadius)
} }
var coordinate: CLLocationCoordinate2D { var coordinate: CLLocationCoordinate2D {

View File

@ -173,8 +173,7 @@ public final class LocationViewController: ViewController {
if reset { if reset {
if let messageId = messageId { if let messageId = messageId {
let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start()
// let _ = cancelProximityNotification(postbox: context.account.postbox, network: context.account.network, messageId: messageId).start()
} }
} else { } else {
strongSelf.controllerNode.setProximityIndicator(radius: 0) strongSelf.controllerNode.setProximityIndicator(radius: 0)
@ -189,8 +188,10 @@ public final class LocationViewController: ViewController {
return return
} }
if let coordinate = coordinate { if let messageId = messageId {
self?.interaction?.sendLiveLocation(coordinate, distance) let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start()
} else if let coordinate = coordinate {
strongSelf.interaction?.sendLiveLocation(coordinate, distance)
} }
completion() completion()
}, willDismiss: { [weak self] in }, willDismiss: { [weak self] in
@ -218,42 +219,44 @@ public final class LocationViewController: ViewController {
return return
} }
let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId) if let distance = distance {
|> deliverOnMainQueue).start(next: { [weak self] peer in params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 30 * 60, proximityNotificationRadius: distance))
let controller = ActionSheetController(presentationData: strongSelf.presentationData) } else {
var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId)
if let user = peer as? TelegramUser { |> deliverOnMainQueue).start(next: { [weak self] peer in
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(user.compactDisplayTitle).0 let controller = ActionSheetController(presentationData: strongSelf.presentationData)
} var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription
if let user = peer as? TelegramUser {
let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(user.compactDisplayTitle).0
controller?.dismissAnimated()
if let strongSelf = self {
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period))
} }
}
let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in
controller.setItemGroups([ controller?.dismissAnimated()
ActionSheetItemGroup(items: [ params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period))
ActionSheetTextItem(title: title), }
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: {
sendLiveLocationImpl(15 * 60) controller.setItemGroups([
}), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { ActionSheetTextItem(title: title),
sendLiveLocationImpl(60 * 60 - 1) ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: {
}), sendLiveLocationImpl(15 * 60)
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { }),
sendLiveLocationImpl(8 * 60 * 60) ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: {
}) sendLiveLocationImpl(60 * 60 - 1)
]), }),
ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: {
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in sendLiveLocationImpl(8 * 60 * 60)
controller?.dismissAnimated() })
}) ]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in
controller?.dismissAnimated()
})
])
]) ])
]) strongSelf.present(controller, in: .window(.root))
strongSelf.present(controller, in: .window(.root)) })
}) }
} }
}, stopLiveLocation: { [weak self] in }, stopLiveLocation: { [weak self] in
params.stopLiveLocation(nil) params.stopLiveLocation(nil)

View File

@ -566,33 +566,117 @@ private extension SemanticStatusNodeState {
private final class SemanticStatusNodeTransitionDrawingState { private final class SemanticStatusNodeTransitionDrawingState {
let transition: CGFloat let transition: CGFloat
let drawingState: SemanticStatusNodeStateDrawingState let drawingState: SemanticStatusNodeStateDrawingState?
let appearanceState: SemanticStatusNodeAppearanceDrawingState?
init(transition: CGFloat, drawingState: SemanticStatusNodeStateDrawingState) { init(transition: CGFloat, drawingState: SemanticStatusNodeStateDrawingState?, appearanceState: SemanticStatusNodeAppearanceDrawingState?) {
self.transition = transition self.transition = transition
self.drawingState = drawingState self.drawingState = drawingState
self.appearanceState = appearanceState
}
}
private final class SemanticStatusNodeAppearanceContext {
let background: UIColor
let foreground: UIColor
let backgroundImage: UIImage?
let overlayForeground: UIColor?
let cutout: CGRect?
init(background: UIColor, foreground: UIColor, backgroundImage: UIImage?, overlayForeground: UIColor?, cutout: CGRect?) {
self.background = background
self.foreground = foreground
self.backgroundImage = backgroundImage
self.overlayForeground = overlayForeground
self.cutout = cutout
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeAppearanceDrawingState {
return SemanticStatusNodeAppearanceDrawingState(transitionFraction: transitionFraction, background: self.background, foreground: self.foreground, backgroundImage: self.backgroundImage, overlayForeground: self.overlayForeground, cutout: self.cutout)
}
func withUpdatedBackground(_ background: UIColor) -> SemanticStatusNodeAppearanceContext {
return SemanticStatusNodeAppearanceContext(background: background, foreground: self.foreground, backgroundImage: self.backgroundImage, overlayForeground: self.overlayForeground, cutout: cutout)
}
func withUpdatedForeground(_ foreground: UIColor) -> SemanticStatusNodeAppearanceContext {
return SemanticStatusNodeAppearanceContext(background: self.background, foreground: foreground, backgroundImage: self.backgroundImage, overlayForeground: self.overlayForeground, cutout: cutout)
}
func withUpdatedBackgroundImage(_ backgroundImage: UIImage?) -> SemanticStatusNodeAppearanceContext {
return SemanticStatusNodeAppearanceContext(background: self.background, foreground: self.foreground, backgroundImage: backgroundImage, overlayForeground: self.overlayForeground, cutout: cutout)
}
func withUpdatedOverlayForeground(_ overlayForeground: UIColor?) -> SemanticStatusNodeAppearanceContext {
return SemanticStatusNodeAppearanceContext(background: self.background, foreground: self.foreground, backgroundImage: self.backgroundImage, overlayForeground: overlayForeground, cutout: cutout)
}
func withUpdatedCutout(_ cutout: CGRect?) -> SemanticStatusNodeAppearanceContext {
return SemanticStatusNodeAppearanceContext(background: self.background, foreground: self.foreground, backgroundImage: self.backgroundImage, overlayForeground: self.overlayForeground, cutout: cutout)
}
}
private final class SemanticStatusNodeAppearanceDrawingState {
let transitionFraction: CGFloat
let background: UIColor
let foreground: UIColor
let backgroundImage: UIImage?
let overlayForeground: UIColor?
let cutout: CGRect?
var effectiveForegroundColor: UIColor {
if let _ = self.backgroundImage, let overlayForeground = self.overlayForeground {
return overlayForeground
} else {
return self.foreground
}
}
init(transitionFraction: CGFloat, background: UIColor, foreground: UIColor, backgroundImage: UIImage?, overlayForeground: UIColor?, cutout: CGRect?) {
self.transitionFraction = transitionFraction
self.background = background
self.foreground = foreground
self.backgroundImage = backgroundImage
self.overlayForeground = overlayForeground
self.cutout = cutout
}
func drawBackground(context: CGContext, size: CGSize) {
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundImage = self.backgroundImage?.cgImage {
context.saveGState()
context.translateBy(x: 0.0, y: bounds.height)
context.scaleBy(x: 1.0, y: -1.0)
context.setAlpha(self.transitionFraction)
context.draw(backgroundImage, in: bounds)
context.restoreGState()
} else {
context.setFillColor(self.background.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: bounds.size))
}
}
func drawForeground(context: CGContext, size: CGSize) {
if let cutout = self.cutout {
let size = CGSize(width: cutout.width * self.transitionFraction, height: cutout.height * self.transitionFraction)
let rect = CGRect(origin: CGPoint(x: cutout.midX - size.width / 2.0, y: cutout.midY - size.height / 2.0), size: size)
context.setBlendMode(.clear)
context.fillEllipse(in: rect)
}
} }
} }
private final class SemanticStatusNodeDrawingState: NSObject { private final class SemanticStatusNodeDrawingState: NSObject {
let background: UIColor
let foreground: UIColor
let hollow: Bool
let transitionState: SemanticStatusNodeTransitionDrawingState? let transitionState: SemanticStatusNodeTransitionDrawingState?
let drawingState: SemanticStatusNodeStateDrawingState let drawingState: SemanticStatusNodeStateDrawingState
let backgroundImage: UIImage? let appearanceState: SemanticStatusNodeAppearanceDrawingState
let overlayForeground: UIColor?
let cutout: SemanticStatusNode.Cutout?
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState, backgroundImage: UIImage?, overlayForeground: UIColor?, cutout: SemanticStatusNode.Cutout?) { init(transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState, appearanceState: SemanticStatusNodeAppearanceDrawingState) {
self.background = background
self.foreground = foreground
self.hollow = hollow
self.transitionState = transitionState self.transitionState = transitionState
self.drawingState = drawingState self.drawingState = drawingState
self.backgroundImage = backgroundImage self.appearanceState = appearanceState
self.overlayForeground = overlayForeground
self.cutout = cutout
super.init() super.init()
} }
@ -601,65 +685,86 @@ private final class SemanticStatusNodeDrawingState: NSObject {
private final class SemanticStatusNodeTransitionContext { private final class SemanticStatusNodeTransitionContext {
let startTime: Double let startTime: Double
let duration: Double let duration: Double
let previousStateContext: SemanticStatusNodeStateContext let previousStateContext: SemanticStatusNodeStateContext?
let previousAppearanceContext: SemanticStatusNodeAppearanceContext?
let completion: () -> Void let completion: () -> Void
init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext, completion: @escaping () -> Void) { init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext?, previousAppearanceContext: SemanticStatusNodeAppearanceContext?, completion: @escaping () -> Void) {
self.startTime = startTime self.startTime = startTime
self.duration = duration self.duration = duration
self.previousStateContext = previousStateContext self.previousStateContext = previousStateContext
self.previousAppearanceContext = previousAppearanceContext
self.completion = completion self.completion = completion
} }
} }
public final class SemanticStatusNode: ASControlNode { public final class SemanticStatusNode: ASControlNode {
final class Cutout {
}
public var backgroundNodeColor: UIColor { public var backgroundNodeColor: UIColor {
didSet { get {
if !self.backgroundNodeColor.isEqual(oldValue) { return self.appearanceContext.background
}
set {
if !self.appearanceContext.background.isEqual(newValue) {
self.appearanceContext = self.appearanceContext.withUpdatedBackground(newValue)
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
} }
public var foregroundNodeColor: UIColor { public var foregroundNodeColor: UIColor {
didSet { get {
if !self.foregroundNodeColor.isEqual(oldValue) { return self.appearanceContext.foreground
}
set {
if !self.appearanceContext.foreground.isEqual(newValue) {
self.appearanceContext = self.appearanceContext.withUpdatedForeground(newValue)
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
} }
public var overlayForegroundNodeColor: UIColor? { public var overlayForegroundNodeColor: UIColor? {
didSet { get {
if !(self.overlayForegroundNodeColor?.isEqual(oldValue) ?? true) { return self.appearanceContext.overlayForeground
}
set {
if !(self.appearanceContext.overlayForeground?.isEqual(newValue) ?? false) {
self.appearanceContext = self.appearanceContext.withUpdatedOverlayForeground(newValue)
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
} }
private let hollow: Bool public var cutout: CGRect? {
get {
return self.appearanceContext.cutout
}
set {
if self.appearanceContext.cutout != newValue {
self.transitionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: nil, previousAppearanceContext: self.appearanceContext, completion: {})
self.appearanceContext = self.appearanceContext.withUpdatedCutout(newValue)
self.updateAnimations()
self.setNeedsDisplay()
}
}
}
private var animator: ConstantDisplayLinkAnimator? private var animator: ConstantDisplayLinkAnimator?
private var hasState: Bool = false private var hasState: Bool = false
public private(set) var state: SemanticStatusNodeState public private(set) var state: SemanticStatusNodeState
private var transtionContext: SemanticStatusNodeTransitionContext? private var transitionContext: SemanticStatusNodeTransitionContext?
private var stateContext: SemanticStatusNodeStateContext private var stateContext: SemanticStatusNodeStateContext
private var appearanceContext: SemanticStatusNodeAppearanceContext
private var disposable: Disposable? private var disposable: Disposable?
private var backgroundNodeImage: UIImage? private var backgroundNodeImage: UIImage?
public init(backgroundNodeColor: UIColor, foregroundNodeColor: UIColor, image: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? = nil, overlayForegroundNodeColor: UIColor? = nil, hollow: Bool = false) { public init(backgroundNodeColor: UIColor, foregroundNodeColor: UIColor, image: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? = nil, overlayForegroundNodeColor: UIColor? = nil, cutout: CGRect? = nil) {
self.backgroundNodeColor = backgroundNodeColor
self.foregroundNodeColor = foregroundNodeColor
self.overlayForegroundNodeColor = overlayForegroundNodeColor
self.hollow = hollow
self.state = .none self.state = .none
self.stateContext = self.state.context(current: nil) self.stateContext = self.state.context(current: nil)
self.appearanceContext = SemanticStatusNodeAppearanceContext(background: backgroundNodeColor, foreground: foregroundNodeColor, backgroundImage: nil, overlayForeground: overlayForegroundNodeColor, cutout: cutout)
super.init() super.init()
@ -667,14 +772,22 @@ public final class SemanticStatusNode: ASControlNode {
self.displaysAsynchronously = true self.displaysAsynchronously = true
if let image = image { if let image = image {
let start = CACurrentMediaTime()
self.disposable = (image self.disposable = (image
|> deliverOnMainQueue).start(next: { [weak self] transform in |> deliverOnMainQueue).start(next: { [weak self] transform in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let context = transform(TransformImageArguments(corners: ImageCorners(radius: strongSelf.bounds.width / 2.0), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets())) let context = transform(TransformImageArguments(corners: ImageCorners(radius: strongSelf.bounds.width / 2.0), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets()))
self?.backgroundNodeImage = context?.generateImage()
self?.setNeedsDisplay() let previousAppearanceContext = strongSelf.appearanceContext
strongSelf.appearanceContext = strongSelf.appearanceContext.withUpdatedBackgroundImage(context?.generateImage())
if CACurrentMediaTime() - start > 0.2 {
strongSelf.transitionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: nil, previousAppearanceContext: previousAppearanceContext, completion: {})
strongSelf.updateAnimations()
}
strongSelf.setNeedsDisplay()
}) })
} }
} }
@ -687,9 +800,9 @@ public final class SemanticStatusNode: ASControlNode {
var animate = false var animate = false
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
if let transtionContext = self.transtionContext { if let transtionContext = self.transitionContext {
if transtionContext.startTime + transtionContext.duration < timestamp { if transtionContext.startTime + transtionContext.duration < timestamp {
self.transtionContext = nil self.transitionContext = nil
transtionContext.completion() transtionContext.completion()
} else { } else {
animate = true animate = true
@ -724,13 +837,13 @@ public final class SemanticStatusNode: ASControlNode {
self.hasState = true self.hasState = true
animated = false animated = false
} }
if self.state != state { if self.state != state || self.appearanceContext.cutout != cutout {
self.state = state self.state = state
let previousStateContext = self.stateContext let previousStateContext = self.stateContext
self.stateContext = self.state.context(current: self.stateContext) self.stateContext = self.state.context(current: self.stateContext)
if animated && previousStateContext !== self.stateContext { if animated && previousStateContext !== self.stateContext {
self.transtionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, completion: completion) self.transitionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, previousAppearanceContext: nil, completion: completion)
} else { } else {
completion() completion()
} }
@ -745,15 +858,23 @@ public final class SemanticStatusNode: ASControlNode {
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionState: SemanticStatusNodeTransitionDrawingState? var transitionState: SemanticStatusNodeTransitionDrawingState?
var transitionFraction: CGFloat = 1.0 var transitionFraction: CGFloat = 1.0
if let transitionContext = self.transtionContext { var appearanceTransitionFraction: CGFloat = 1.0
if let transitionContext = self.transitionContext {
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration) var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
t = min(1.0, max(0.0, t)) t = min(1.0, max(0.0, t))
transitionFraction = t
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t)) if let _ = transitionContext.previousStateContext {
transitionFraction = t
}
if let _ = transitionContext.previousAppearanceContext {
appearanceTransitionFraction = t
}
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext?.drawingState(transitionFraction: 1.0 - t), appearanceState: transitionContext.previousAppearanceContext?.drawingState(transitionFraction: 1.0 - t))
} }
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction), backgroundImage: self.backgroundNodeImage, overlayForeground: self.overlayForegroundNodeColor, cutout: nil) return SemanticStatusNodeDrawingState(transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction), appearanceState: self.appearanceContext.drawingState(transitionFraction: appearanceTransitionFraction))
} }
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
@ -768,32 +889,20 @@ public final class SemanticStatusNode: ASControlNode {
guard let parameters = parameters as? SemanticStatusNodeDrawingState else { guard let parameters = parameters as? SemanticStatusNodeDrawingState else {
return return
} }
var foregroundColor = parameters.foreground if let transitionAppearanceState = parameters.transitionState?.appearanceState {
if let backgroundImage = parameters.backgroundImage?.cgImage { transitionAppearanceState.drawBackground(context: context, size: bounds.size)
context.saveGState()
context.translateBy(x: 0.0, y: bounds.height)
context.scaleBy(x: 1.0, y: -1.0)
context.draw(backgroundImage, in: bounds)
context.restoreGState()
if let overlayForegroundColor = parameters.overlayForeground {
foregroundColor = overlayForegroundColor
} else {
foregroundColor = .white
}
} else {
context.setFillColor(parameters.background.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: bounds.size))
} }
if let transitionState = parameters.transitionState { parameters.appearanceState.drawBackground(context: context, size: bounds.size)
transitionState.drawingState.draw(context: context, size: bounds.size, foregroundColor: foregroundColor)
if let transitionDrawingState = parameters.transitionState?.drawingState {
transitionDrawingState.draw(context: context, size: bounds.size, foregroundColor: parameters.appearanceState.effectiveForegroundColor)
} }
parameters.drawingState.draw(context: context, size: bounds.size, foregroundColor: foregroundColor) parameters.drawingState.draw(context: context, size: bounds.size, foregroundColor: parameters.appearanceState.effectiveForegroundColor)
if parameters.hollow { if let transitionAppearanceState = parameters.transitionState?.appearanceState {
context.setBlendMode(.clear) transitionAppearanceState.drawForeground(context: context, size: bounds.size)
context.fillEllipse(in: bounds.insetBy(dx: 8.0, dy: 8.0))
} }
parameters.appearanceState.drawForeground(context: context, size: bounds.size)
} }
} }

View File

@ -133,14 +133,15 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
self.currentContent = content self.currentContent = content
if content.hasProgress { if content.hasProgress {
let statusFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
if self.statusNode == nil { if self.statusNode == nil {
let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, hollow: true) let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, cutout: statusFrame.insetBy(dx: 8.0, dy: 8.0))
self.statusNode = statusNode self.statusNode = statusNode
self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode) 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: {}) statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0)), animated: false, completion: {})
} }
if let statusNode = self.statusNode { if let statusNode = self.statusNode {
statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) statusNode.frame = statusFrame
if transition.isAnimated { if transition.isAnimated {
statusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) statusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)

View File

@ -146,12 +146,15 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods:
if let _ = map.heading { if let _ = map.heading {
flags |= 1 << 2 flags |= 1 << 2
} }
if let _ = map.liveProximityNotificationRadius {
flags |= 1 << 3
}
var geoFlags: Int32 = 0 var geoFlags: Int32 = 0
if let _ = map.accuracyRadius { if let _ = map.accuracyRadius {
geoFlags |= 1 << 0 geoFlags |= 1 << 0
} }
if let liveBroadcastingTimeout = map.liveBroadcastingTimeout { if let liveBroadcastingTimeout = map.liveBroadcastingTimeout {
input = .inputMediaGeoLive(flags: flags, geoPoint: Api.InputGeoPoint.inputGeoPoint(flags: geoFlags, lat: map.latitude, long: map.longitude, accuracyRadius: map.accuracyRadius.flatMap({ Int32($0) })), heading: map.heading, period: liveBroadcastingTimeout, proximityNotificationRadius: nil) input = .inputMediaGeoLive(flags: flags, geoPoint: Api.InputGeoPoint.inputGeoPoint(flags: geoFlags, lat: map.latitude, long: map.longitude, accuracyRadius: map.accuracyRadius.flatMap({ Int32($0) })), heading: map.heading, period: liveBroadcastingTimeout, proximityNotificationRadius: map.liveProximityNotificationRadius.flatMap({ Int32($0) }))
} else if let venue = map.venue { } else if let venue = map.venue {
input = .inputMediaVenue(geoPoint: Api.InputGeoPoint.inputGeoPoint(flags: geoFlags, lat: map.latitude, long: map.longitude, accuracyRadius: map.accuracyRadius.flatMap({ Int32($0) })), title: venue.title, address: venue.address ?? "", provider: venue.provider ?? "", venueId: venue.id ?? "", venueType: venue.type ?? "") input = .inputMediaVenue(geoPoint: Api.InputGeoPoint.inputGeoPoint(flags: geoFlags, lat: map.latitude, long: map.longitude, accuracyRadius: map.accuracyRadius.flatMap({ Int32($0) })), title: venue.title, address: venue.address ?? "", provider: venue.provider ?? "", venueId: venue.id ?? "", venueType: venue.type ?? "")
} else { } else {

View File

@ -255,7 +255,7 @@ private func requestEditMessageInternal(postbox: Postbox, network: Network, stat
} }
} }
public func requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?) -> Signal<Void, NoError> { public func requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> (Api.InputPeer, TelegramMediaMap)? in return postbox.transaction { transaction -> (Api.InputPeer, TelegramMediaMap)? in
guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {
return nil return nil
@ -275,19 +275,33 @@ public func requestEditLiveLocation(postbox: Postbox, network: Network, stateMan
return .complete() return .complete()
} }
let inputMedia: Api.InputMedia let inputMedia: Api.InputMedia
if let coordinate = coordinate, let liveBroadcastingTimeout = media.liveBroadcastingTimeout { if let liveBroadcastingTimeout = media.liveBroadcastingTimeout, !stop {
var flags: Int32 = 1 << 1 var flags: Int32 = 1 << 1
if let _ = media.heading { let inputGeoPoint: Api.InputGeoPoint
if let coordinate = coordinate {
var geoFlags: Int32 = 0
if let _ = coordinate.accuracyRadius {
geoFlags |= 1 << 0
}
inputGeoPoint = .inputGeoPoint(flags: geoFlags, lat: coordinate.latitude, long: coordinate.longitude, accuracyRadius: coordinate.accuracyRadius.flatMap({ Int32($0) }))
} else {
var geoFlags: Int32 = 0
if let _ = media.accuracyRadius {
geoFlags |= 1 << 0
}
inputGeoPoint = .inputGeoPoint(flags: geoFlags, lat: media.latitude, long: media.longitude, accuracyRadius: media.accuracyRadius.flatMap({ Int32($0) }))
}
if let _ = heading {
flags |= 1 << 2 flags |= 1 << 2
} }
var geoFlags: Int32 = 0 if let _ = proximityNotificationRadius {
if let _ = coordinate.accuracyRadius { flags |= 1 << 3
geoFlags |= 1 << 0
} }
inputMedia = .inputMediaGeoLive(flags: flags, geoPoint: .inputGeoPoint(flags: geoFlags, lat: coordinate.latitude, long: coordinate.longitude, accuracyRadius: coordinate.accuracyRadius.flatMap({ Int32($0) })), heading: heading, period: liveBroadcastingTimeout, proximityNotificationRadius: nil) inputMedia = .inputMediaGeoLive(flags: flags, geoPoint: inputGeoPoint, heading: heading, period: liveBroadcastingTimeout, proximityNotificationRadius: proximityNotificationRadius)
} else { } else {
inputMedia = .inputMediaGeoLive(flags: 1 << 0, geoPoint: .inputGeoPoint(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil), heading: nil, period: nil, proximityNotificationRadius: nil) inputMedia = .inputMediaGeoLive(flags: 1 << 0, geoPoint: .inputGeoPoint(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil), heading: nil, period: nil, proximityNotificationRadius: nil)
} }
return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil)) return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in |> `catch` { _ -> Signal<Api.Updates?, NoError> in
@ -297,7 +311,7 @@ public func requestEditLiveLocation(postbox: Postbox, network: Network, stateMan
if let updates = updates { if let updates = updates {
stateManager.addUpdates(updates) stateManager.addUpdates(updates)
} }
if coordinate == nil { if coordinate == nil && proximityNotificationRadius == nil {
return postbox.transaction { transaction -> Void in return postbox.transaction { transaction -> Void in
transaction.updateMessage(messageId, update: { currentMessage in transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo? var storeForwardInfo: StoreMessageForwardInfo?

View File

@ -77,6 +77,7 @@ public func chatBubbleActionButtonImage(fillColor: UIColor, strokeColor: UIColor
public final class PrincipalThemeEssentialGraphics { public final class PrincipalThemeEssentialGraphics {
public let chatMessageBackgroundIncomingMaskImage: UIImage public let chatMessageBackgroundIncomingMaskImage: UIImage
public let chatMessageBackgroundIncomingExtractedMaskImage: UIImage
public let chatMessageBackgroundIncomingImage: UIImage public let chatMessageBackgroundIncomingImage: UIImage
public let chatMessageBackgroundIncomingExtractedImage: UIImage public let chatMessageBackgroundIncomingExtractedImage: UIImage
public let chatMessageBackgroundIncomingOutlineImage: UIImage public let chatMessageBackgroundIncomingOutlineImage: UIImage
@ -110,6 +111,7 @@ public final class PrincipalThemeEssentialGraphics {
public let chatMessageBackgroundIncomingMergedSideHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedSideHighlightedImage: UIImage
public let chatMessageBackgroundOutgoingMaskImage: UIImage public let chatMessageBackgroundOutgoingMaskImage: UIImage
public let chatMessageBackgroundOutgoingExtractedMaskImage: UIImage
public let chatMessageBackgroundOutgoingImage: UIImage public let chatMessageBackgroundOutgoingImage: UIImage
public let chatMessageBackgroundOutgoingExtractedImage: UIImage public let chatMessageBackgroundOutgoingExtractedImage: UIImage
public let chatMessageBackgroundOutgoingOutlineImage: UIImage public let chatMessageBackgroundOutgoingOutlineImage: UIImage
@ -248,12 +250,14 @@ public final class PrincipalThemeEssentialGraphics {
let emptyImage = UIImage() let emptyImage = UIImage()
if preview { if preview {
self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true)
self.chatMessageBackgroundIncomingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true)
self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true)
self.chatMessageBackgroundIncomingExtractedOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingExtractedOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true)
self.chatMessageBackgroundIncomingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundIncomingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true)
self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundOutgoingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true)
self.chatMessageBackgroundOutgoingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true)
self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true)
@ -350,6 +354,7 @@ public final class PrincipalThemeEssentialGraphics {
self.radialIndicatorFileIconOutgoing = emptyImage self.radialIndicatorFileIconOutgoing = emptyImage
} else { } else {
self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true)
self.chatMessageBackgroundIncomingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true)
self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true)
@ -378,6 +383,7 @@ public final class PrincipalThemeEssentialGraphics {
self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true)
self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundOutgoingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true)
self.chatMessageBackgroundOutgoingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true)
self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true)

View File

@ -28,7 +28,7 @@ func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: PrincipalThe
case .Side: case .Side:
image = graphics.chatMessageBackgroundIncomingMergedSideMaskImage image = graphics.chatMessageBackgroundIncomingMergedSideMaskImage
case .Extracted: case .Extracted:
image = nil image = graphics.chatMessageBackgroundIncomingExtractedMaskImage
} }
case let .outgoing(mergeType): case let .outgoing(mergeType):
switch mergeType { switch mergeType {
@ -47,7 +47,7 @@ func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: PrincipalThe
case .Side: case .Side:
image = graphics.chatMessageBackgroundOutgoingMergedSideMaskImage image = graphics.chatMessageBackgroundOutgoingMergedSideMaskImage
case .Extracted: case .Extracted:
image = nil image = graphics.chatMessageBackgroundOutgoingExtractedMaskImage
} }
} }
return image return image

View File

@ -214,10 +214,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let contentMessageStableId: UInt32 let contentMessageStableId: UInt32
let sourceNode: ContextExtractedContentContainingNode let sourceNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode let containerNode: ContextControllerSourceNode
var backgroundWallpaperNode: ChatMessageBubbleBackdrop?
var backgroundNode: ChatMessageBackground? var backgroundNode: ChatMessageBackground?
var selectionBackgroundNode: ASDisplayNode? var selectionBackgroundNode: ASDisplayNode?
var currentParams: (size: CGSize, contentOrigin: CGPoint, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, Bool?, selectionInsets: UIEdgeInsets)? private var currentParams: (size: CGSize, contentOrigin: CGPoint, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, mediaBox: MediaBox, messageSelection: Bool?, selectionInsets: UIEdgeInsets)?
init(contentMessageStableId: UInt32) { init(contentMessageStableId: UInt32) {
self.contentMessageStableId = contentMessageStableId self.contentMessageStableId = contentMessageStableId
@ -226,7 +227,31 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
} }
func willUpdateIsExtractedToContextPreview(isExtractedToContextPreview: Bool, transition: ContainedViewLayoutTransition) { private var absoluteRect: (CGRect, CGSize)?
fileprivate func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
return
}
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + backgroundWallpaperNode.frame.minX, y: rect.minY + backgroundWallpaperNode.frame.minY), size: rect.size)
backgroundWallpaperNode.update(rect: mappedRect, within: containerSize)
}
fileprivate func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
return
}
backgroundWallpaperNode.offset(value: value, animationCurve: animationCurve, duration: duration)
}
fileprivate func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
return
}
backgroundWallpaperNode.offsetSpring(value: value, duration: duration, damping: damping)
}
fileprivate func willUpdateIsExtractedToContextPreview(isExtractedToContextPreview: Bool, transition: ContainedViewLayoutTransition) {
if isExtractedToContextPreview { if isExtractedToContextPreview {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var inset: CGFloat = 0.0 var inset: CGFloat = 0.0
@ -242,34 +267,57 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if let _ = self.backgroundNode { if let _ = self.backgroundNode {
} else if let currentParams = self.currentParams { } else if let currentParams = self.currentParams {
let backgroundWallpaperNode = ChatMessageBubbleBackdrop()
backgroundWallpaperNode.alpha = 0.0
let backgroundNode = ChatMessageBackground() let backgroundNode = ChatMessageBackground()
backgroundNode.alpha = 0.0 backgroundNode.alpha = 0.0
backgroundNode.setType(type: type, highlighted: false, graphics: currentParams.graphics, maskMode: false, hasWallpaper: currentParams.presentationData.theme.wallpaper.hasWallpaper, transition: .immediate)
self.sourceNode.contentNode.insertSubnode(backgroundNode, at: 0) self.sourceNode.contentNode.insertSubnode(backgroundNode, at: 0)
self.sourceNode.contentNode.insertSubnode(backgroundWallpaperNode, at: 0)
self.backgroundWallpaperNode = backgroundWallpaperNode
self.backgroundNode = backgroundNode self.backgroundNode = backgroundNode
transition.updateAlpha(node: backgroundNode, alpha: 1.0) transition.updateAlpha(node: backgroundNode, alpha: 1.0)
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 1.0)
backgroundNode.setType(type: type, highlighted: false, graphics: currentParams.graphics, maskMode: true, hasWallpaper: currentParams.presentationData.theme.wallpaper.hasWallpaper, transition: .immediate)
backgroundWallpaperNode.setType(type: type, theme: currentParams.presentationData.theme, mediaBox: currentParams.mediaBox, essentialGraphics: currentParams.graphics, maskMode: true)
} }
if let currentParams = self.currentParams { if let currentParams = self.currentParams {
let backgroundFrame = CGRect(x: currentParams.contentOrigin.x + offset, y: 0.0, width: currentParams.size.width + inset, height: currentParams.size.height) let backgroundFrame = CGRect(x: currentParams.contentOrigin.x + offset, y: 0.0, width: currentParams.size.width + inset, height: currentParams.size.height)
self.backgroundNode?.updateLayout(size: backgroundFrame.size, transition: .immediate) self.backgroundNode?.updateLayout(size: backgroundFrame.size, transition: .immediate)
self.backgroundNode?.frame = backgroundFrame self.backgroundNode?.frame = backgroundFrame
self.backgroundWallpaperNode?.frame = backgroundFrame
if let (rect, containerSize) = self.absoluteRect {
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + backgroundFrame.minX, y: rect.minY + backgroundFrame.minY), size: rect.size)
self.backgroundWallpaperNode?.update(rect: mappedRect, within: containerSize)
}
}
} else {
if let backgroundNode = self.backgroundNode {
self.backgroundNode = nil
transition.updateAlpha(node: backgroundNode, alpha: 0.0, completion: { [weak backgroundNode] _ in
backgroundNode?.removeFromSupernode()
})
}
if let backgroundWallpaperNode = self.backgroundWallpaperNode {
self.backgroundWallpaperNode = nil
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 0.0, completion: { [weak backgroundWallpaperNode] _ in
backgroundWallpaperNode?.removeFromSupernode()
})
} }
} else if let backgroundNode = self.backgroundNode {
self.backgroundNode = nil
transition.updateAlpha(node: backgroundNode, alpha: 0.0, completion: { [weak backgroundNode] _ in
backgroundNode?.removeFromSupernode()
})
} }
} }
func isExtractedToContextPreviewUpdated(_ isExtractedToContextPreview: Bool) { fileprivate func isExtractedToContextPreviewUpdated(_ isExtractedToContextPreview: Bool) {
} }
func update(size: CGSize, contentOrigin: CGPoint, selectionInsets: UIEdgeInsets, index: Int, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, messageSelection: Bool?) { fileprivate func update(size: CGSize, contentOrigin: CGPoint, selectionInsets: UIEdgeInsets, index: Int, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, mediaBox: MediaBox, messageSelection: Bool?) {
self.currentParams = (size, contentOrigin, presentationData, graphics, backgroundType, messageSelection, selectionInsets) self.currentParams = (size, contentOrigin, presentationData, graphics, backgroundType, mediaBox, messageSelection, selectionInsets)
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
var incoming: Bool = false var incoming: Bool = false
@ -2314,9 +2362,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
container?.isExtractedToContextPreviewUpdated(isExtractedToContextPreview) container?.isExtractedToContextPreviewUpdated(isExtractedToContextPreview)
// if !isExtractedToContextPreview, let (rect, size) = strongSelf.absoluteRect { if !isExtractedToContextPreview, let (rect, size) = strongSelf.absoluteRect {
// strongSelf.updateAbsoluteRect(rect, within: size) container?.updateAbsoluteRect(relativeFrame.offsetBy(dx: rect.minX, dy: rect.minY), within: size)
// } }
for contentNode in strongSelf.contentNodes { for contentNode in strongSelf.contentNodes {
if contentNode.supernode === strongContextSourceNode.contentNode { if contentNode.supernode === strongContextSourceNode.contentNode {
@ -2325,23 +2373,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
} }
contextSourceNode.updateAbsoluteRect = { [weak strongSelf] rect, size in contextSourceNode.updateAbsoluteRect = { [weak strongSelf, weak container, weak contextSourceNode] rect, size in
guard let strongSelf = strongSelf, strongSelf.mainContextSourceNode.isExtractedToContextPreview else { guard let strongSelf = strongSelf, let strongContextSourceNode = contextSourceNode, strongContextSourceNode.isExtractedToContextPreview else {
return return
} }
// strongSelf.updateAbsoluteRectInternal(rect, within: size) container?.updateAbsoluteRect(relativeFrame.offsetBy(dx: rect.minX, dy: rect.minY), within: size)
} }
contextSourceNode.applyAbsoluteOffset = { [weak strongSelf] value, animationCurve, duration in contextSourceNode.applyAbsoluteOffset = { [weak strongSelf, weak container, weak contextSourceNode] value, animationCurve, duration in
guard let strongSelf = strongSelf, strongSelf.mainContextSourceNode.isExtractedToContextPreview else { guard let strongSelf = strongSelf, let strongContextSourceNode = contextSourceNode, strongContextSourceNode.isExtractedToContextPreview else {
return return
} }
// strongSelf.applyAbsoluteOffsetInternal(value: value, animationCurve: animationCurve, duration: duration) container?.applyAbsoluteOffset(value: value, animationCurve: animationCurve, duration: duration)
} }
contextSourceNode.applyAbsoluteOffsetSpring = { [weak strongSelf] value, duration, damping in contextSourceNode.applyAbsoluteOffsetSpring = { [weak strongSelf, weak container, weak contextSourceNode] value, duration, damping in
guard let strongSelf = strongSelf, strongSelf.mainContextSourceNode.isExtractedToContextPreview else { guard let strongSelf = strongSelf, let strongContextSourceNode = contextSourceNode, strongContextSourceNode.isExtractedToContextPreview else {
return return
} }
// strongSelf.applyAbsoluteOffsetSpringInternal(value: value, duration: duration, damping: damping) container?.applyAbsoluteOffsetSpring(value: value, duration: duration, damping: damping)
} }
strongSelf.contentContainers.append(container) strongSelf.contentContainers.append(container)
@ -2371,7 +2419,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
selectionInsets.bottom = groupOverlap / 2.0 selectionInsets.bottom = groupOverlap / 2.0
} }
contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, messageSelection: itemSelection) contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection)
index += 1 index += 1
} }

View File

@ -24,7 +24,6 @@ private struct FetchControls {
final class ChatMessageInteractiveFileNode: ASDisplayNode { final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var selectionNode: FileMessageSelectionNode? private var selectionNode: FileMessageSelectionNode?
private var cutoutNode: ASDisplayNode?
private let titleNode: TextNode private let titleNode: TextNode
private let descriptionNode: TextNode private let descriptionNode: TextNode
@ -974,34 +973,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
} }
let cutoutFrame = streamingCacheStatusFrame.insetBy(dx: -(1.0 + UIScreenPixel), dy: -(1.0 + UIScreenPixel)).offsetBy(dx: progressFrame.minX - 6.0, dy: progressFrame.minY)
if streamingState == .none && self.selectionNode == nil { if streamingState == .none && self.selectionNode == nil {
if let cutoutNode = self.cutoutNode { self.statusNode?.cutout = nil
self.cutoutNode = nil
if animated {
cutoutNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) { [weak cutoutNode] _ in
cutoutNode?.removeFromSupernode()
}
} else {
cutoutNode.removeFromSupernode()
}
}
} else if let statusNode = self.statusNode, (self.iconNode?.isHidden ?? true) { } else if let statusNode = self.statusNode, (self.iconNode?.isHidden ?? true) {
if let _ = self.cutoutNode { self.statusNode?.cutout = cutoutFrame
} else {
let cutoutNode = ASImageNode()
cutoutNode.displaysAsynchronously = false
cutoutNode.displayWithoutProcessing = true
cutoutNode.image = generateFilledCircleImage(diameter: 23.0, color: messageTheme.bubble.withWallpaper.fill)
self.cutoutNode = cutoutNode
self.insertSubnode(cutoutNode, aboveSubnode: statusNode)
cutoutNode.frame = streamingCacheStatusFrame.insetBy(dx: -(1.0 + UIScreenPixel), dy: -(1.0 + UIScreenPixel))
if animated {
cutoutNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
}
} }
if let (expandedString, compactString, font) = downloadingStrings { if let (expandedString, compactString, font) = downloadingStrings {