Voice Chats UI improvements

This commit is contained in:
Ilya Laktyushin 2020-11-21 13:19:30 +04:00
parent e87908a532
commit a362cab636
22 changed files with 6710 additions and 4725 deletions

View File

@ -5895,3 +5895,42 @@ Sorry for the inconvenience.";
"Conversation.EditingPhotoPanelTitle" = "Edit Photo"; "Conversation.EditingPhotoPanelTitle" = "Edit Photo";
"Conversation.TextCopied" = "Text copied to clipboard"; "Conversation.TextCopied" = "Text copied to clipboard";
"Media.LimitedAccessTitle" = "Limited Access to Media";
"Media.LimitedAccessText" = "You've given Telegram access only to select number of photos.";
"Media.LimitedAccessManage" = "Manage";
"VoiceChat.BackTitle" = "Chat";
"VoiceChat.StatusSpeaking" = "speaking";
"VoiceChat.StatusListening" = "listening";
"VoiceChat.Connecting" = "Connecting...";
"VoiceChat.Reconnecting" = "Reconnecting...";
"VoiceChat.Unmute" = "Unmute";
"VoiceChat.UnmuteHelp" = "or hold and speak";
"VoiceChat.Live" = "You're Live";
"VoiceChat.Mute" = "Tap to Mute";
"VoiceChat.Muted" = "Muted";
"VoiceChat.MutedHelp" = "You are in Listen Only mode";
"VoiceChat.Audio" = "audio";
"VoiceChat.Leave" = "leave";
"VoiceChat.SpeakPermissionEveryone" = "All Members Can Speak";
"VoiceChat.SpeakPermissionAdmin" = "Only Admins Can Speak";
"VoiceChat.Share" = "Share Invite Link";
"VoiceChat.EndVoiceChat" = "End Voice Chat";
"VoiceChat.UnmutePeer" = "Allow to Speak";
"VoiceChat.MutePeer" = "Mute";
"VoiceChat.RemovePeer" = "Remove";
"VoiceChat.RemovePeerConfirmation" = "Are you sure you want to remove %@ from the group chat?";
"VoiceChat.RemovePeerRemove" = "Remove";
"VoiceChat.UserInvited" = "You invited %@ to the voice chat";
"Notification.VoiceChatInvitation" = "%1$@ invited %2$@ to the voice chat";
"Notification.VoiceChatInvitationByYou" = "You invited %1$@ to the voice chat";
"Notification.VoiceChatInvitationForYou" = "%1$@ invited you to the voice chat";

View File

@ -14,6 +14,7 @@ public enum DeleteChatPeerAction {
case clearHistory case clearHistory
case clearCache case clearCache
case clearCacheSuggestion case clearCacheSuggestion
case removeFromGroup
} }
private let avatarFont = avatarPlaceholderFont(size: 26.0) private let avatarFont = avatarPlaceholderFont(size: 26.0)
@ -115,6 +116,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} }
case .clearHistory: case .clearHistory:
text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
case .removeFromGroup:
text = strings.VoiceChat_RemovePeerConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
default: default:
break break
} }

View File

@ -283,6 +283,7 @@ public final class ShareController: ViewController {
private var currentAccount: Account private var currentAccount: Account
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
private let forcedTheme: PresentationTheme?
private let externalShare: Bool private let externalShare: Bool
private let immediateExternalShare: Bool private let immediateExternalShare: Bool
@ -302,11 +303,11 @@ public final class ShareController: ViewController {
public var dismissed: ((Bool) -> Void)? public var dismissed: ((Bool) -> Void)?
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId) self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forcedTheme: forcedTheme)
} }
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil) {
self.sharedContext = sharedContext self.sharedContext = sharedContext
self.currentContext = currentContext self.currentContext = currentContext
self.currentAccount = currentContext.account self.currentAccount = currentContext.account
@ -318,8 +319,12 @@ public final class ShareController: ViewController {
self.immediatePeerId = immediatePeerId self.immediatePeerId = immediatePeerId
self.openStats = openStats self.openStats = openStats
self.shares = shares self.shares = shares
self.forcedTheme = forcedTheme
self.presentationData = self.sharedContext.currentPresentationData.with { $0 } self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -441,7 +446,7 @@ public final class ShareController: ViewController {
return return
} }
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares) }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares, forcedTheme: self.forcedTheme)
self.controllerNode.dismiss = { [weak self] shared in self.controllerNode.dismiss = { [weak self] shared in
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)
self?.dismissed?(shared) self?.dismissed?(shared)

View File

@ -29,6 +29,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private let sharedContext: SharedAccountContext private let sharedContext: SharedAccountContext
private var context: AccountContext? private var context: AccountContext?
private var presentationData: PresentationData private var presentationData: PresentationData
private let forcedTheme: PresentationTheme?
private let externalShare: Bool private let externalShare: Bool
private let immediateExternalShare: Bool private let immediateExternalShare: Bool
private var immediatePeerId: PeerId? private var immediatePeerId: PeerId?
@ -80,9 +81,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private let presetText: String? private let presetText: String?
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?) { init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?, forcedTheme: PresentationTheme?) {
self.sharedContext = sharedContext self.sharedContext = sharedContext
self.presentationData = sharedContext.currentPresentationData.with { $0 } self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.forcedTheme = forcedTheme
self.externalShare = externalShare self.externalShare = externalShare
self.immediateExternalShare = immediateExternalShare self.immediateExternalShare = immediateExternalShare
self.immediatePeerId = immediatePeerId self.immediatePeerId = immediatePeerId
@ -94,6 +96,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.defaultAction = defaultAction self.defaultAction = defaultAction
self.requestLayout = requestLayout self.requestLayout = requestLayout
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
@ -260,6 +266,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
return return
} }
self.presentationData = presentationData self.presentationData = presentationData
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)

View File

@ -27,6 +27,11 @@ swift_library(
"//submodules/ItemListPeerItem:ItemListPeerItem", "//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/MergeLists:MergeLists", "//submodules/MergeLists:MergeLists",
"//submodules/RadialStatusNode:RadialStatusNode", "//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/ContextUI:ContextUI",
"//submodules/ShareController:ShareController",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
"//submodules/AnimationUI:AnimationUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -62,7 +62,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
private let contentNode: ASImageNode private let contentNode: ASImageNode
private let overlayHighlightNode: ASImageNode private let overlayHighlightNode: ASImageNode
private var statusNode: SemanticStatusNode? private var statusNode: SemanticStatusNode?
private let textNode: ImmediateTextNode let textNode: ImmediateTextNode
private let largeButtonSize: CGFloat = 72.0 private let largeButtonSize: CGFloat = 72.0
@ -199,7 +199,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
case .green: case .green:
fillColor = UIColor(rgb: 0x74db58) fillColor = UIColor(rgb: 0x74db58)
case .redDimmed: case .redDimmed:
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.3) fillColor = UIColor(rgb: 0x4d120e)
case .greenDimmed: case .greenDimmed:
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3) fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3)
case .grayDimmed: case .grayDimmed:

View File

@ -0,0 +1,744 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private extension UIBezierPath {
static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat) -> UIBezierPath {
var smoothPoints = [SmoothPoint]()
for index in (0 ..< points.count) {
let prevIndex = index - 1
let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex]
let curr = points[index]
let next = points[(index + 1) % points.count]
let angle: CGFloat = {
let dx = next.x - prev.x
let dy = -next.y + prev.y
let angle = atan2(dy, dx)
if angle < 0 {
return abs(angle)
} else {
return 2 * .pi - angle
}
}()
smoothPoints.append(
SmoothPoint(
point: curr,
inAngle: angle + .pi,
inLength: smoothness * distance(from: curr, to: prev),
outAngle: angle,
outLength: smoothness * distance(from: curr, to: next)
)
)
}
let resultPath = UIBezierPath()
resultPath.move(to: smoothPoints[0].point)
for index in (0 ..< smoothPoints.count) {
let curr = smoothPoints[index]
let next = smoothPoints[(index + 1) % points.count]
let currSmoothOut = curr.smoothOut()
let nextSmoothIn = next.smoothIn()
resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn)
}
resultPath.close()
return resultPath
}
static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat {
return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y))
}
struct SmoothPoint {
let point: CGPoint
let inAngle: CGFloat
let inLength: CGFloat
let outAngle: CGFloat
let outLength: CGFloat
func smoothIn() -> CGPoint {
return smooth(angle: inAngle, length: inLength)
}
func smoothOut() -> CGPoint {
return smooth(angle: outAngle, length: outLength)
}
private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint {
return CGPoint(
x: point.x + length * cos(angle),
y: point.y + length * sin(angle)
)
}
}
}
private final class BlobNodeDrawingState: NSObject {
let scale: CGFloat
let shape: CGPath?
let gradientTransition: CGFloat
let gradientMovement: CGFloat
init(scale: CGFloat, shape: CGPath?, gradientTransition: CGFloat, gradientMovement: CGFloat) {
self.scale = scale
self.shape = shape
self.gradientTransition = gradientTransition
self.gradientMovement = gradientMovement
super.init()
}
}
private class BlobNode: ASDisplayNode {
let size: CGSize
let pointsCount: Int
let smoothness: CGFloat
let minRandomness: CGFloat
let maxRandomness: CGFloat
let minSpeed: CGFloat
let maxSpeed: CGFloat
var minScale: CGFloat
var maxScale: CGFloat
let scaleSpeed: CGFloat
let isCircle: Bool
var currentScale: CGFloat = 1.0
var level: CGFloat = 0.0 {
didSet {
var effectiveMaxScale = maxScale
var effectiveMinScale = minScale
if !self.loop {
effectiveMaxScale *= self.isCircle ? 0.97 : 1.1
effectiveMinScale *= self.isCircle ? 0.97 : 1.1
}
self.currentScale = effectiveMinScale + (effectiveMaxScale - effectiveMinScale) * self.level
}
}
private var speedLevel: CGFloat = 0.0
private var lastSpeedLevel: CGFloat = 0.0
private var fromPoints: [CGPoint]?
private var toPoints: [CGPoint]?
var fromLoop: Bool?
var loop = false {
didSet {
if self.loop != oldValue {
self.fromLoop = oldValue
gradientTransitionArguments = (CACurrentMediaTime(), 0.4)
}
}
}
private var currentPoints: [CGPoint]? {
guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil }
return fromPoints.enumerated().map { offset, fromPoint in
let toPoint = toPoints[offset]
return CGPoint(x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, y: fromPoint.y + (toPoint.y - fromPoint.y) * transition)
}
}
private var currentShape: CGPath?
private var transition: CGFloat = 0 {
didSet {
if let currentPoints = self.currentPoints {
self.currentShape = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness).cgPath
}
}
}
private var gradientTransition: CGFloat = 0.0
private var gradientTransitionArguments: (startTime: Double, duration: Double)?
private var gradientMovementTransition: CGFloat = 0.0
private var gradientMovementTransitionArguments: (startTime: Double, duration: Double, reverse: Bool)?
private var animator: ConstantDisplayLinkAnimator?
private var transitionArguments: (startTime: Double, duration: Double)?
init(
size: CGSize,
pointsCount: Int,
minRandomness: CGFloat,
maxRandomness: CGFloat,
minSpeed: CGFloat,
maxSpeed: CGFloat,
minScale: CGFloat,
maxScale: CGFloat,
scaleSpeed: CGFloat,
isCircle: Bool
) {
self.size = size
self.pointsCount = pointsCount
self.minRandomness = minRandomness
self.maxRandomness = maxRandomness
self.minSpeed = minSpeed
self.maxSpeed = maxSpeed
self.minScale = minScale
self.maxScale = maxScale
self.scaleSpeed = scaleSpeed
self.isCircle = isCircle
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.currentScale = minScale
}
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
speedLevel = max(speedLevel, newSpeedLevel)
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
animateToNewShape()
}
}
func startAnimating() {
animateToNewShape()
}
func stopAnimating() {
fromPoints = currentPoints
toPoints = nil
pop_removeAnimation(forKey: "blob")
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
if let (startTime, duration) = self.gradientTransitionArguments, duration > 0.0 {
if let fromLoop = self.fromLoop {
if fromLoop {
self.gradientTransition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
} else {
self.gradientTransition = max(0.0, min(1.0, 1.0 - CGFloat((timestamp - startTime) / duration)))
}
}
if self.gradientTransition < 1.0 {
animate = true
} else {
self.gradientTransitionArguments = nil
}
}
if let (startTime, duration) = self.transitionArguments, duration > 0.0 {
self.transition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
if self.transition < 1.0 {
animate = true
} else {
if self.loop {
self.animateToNewShape()
} else {
self.fromPoints = self.currentPoints
self.toPoints = nil
self.transition = 0.0
self.transitionArguments = nil
}
}
}
let gradientMovementStartTime: Double
let gradientMovementDuration: Double
let gradientMovementReverse: Bool
if let (startTime, duration, reverse) = self.gradientMovementTransitionArguments, duration > 0.0 {
gradientMovementStartTime = startTime
gradientMovementDuration = duration
gradientMovementReverse = reverse
} else {
gradientMovementStartTime = CACurrentMediaTime()
gradientMovementDuration = 1.0
gradientMovementReverse = false
self.gradientMovementTransitionArguments = (gradientMovementStartTime, gradientMovementStartTime, gradientMovementReverse)
}
let movementT = CGFloat((timestamp - gradientMovementStartTime) / gradientMovementDuration)
self.gradientMovementTransition = gradientMovementReverse ? 1.0 - movementT : movementT
if gradientMovementReverse && self.gradientMovementTransition <= 0.0 {
self.gradientMovementTransitionArguments = (CACurrentMediaTime(), 1.0, false)
} else if !gradientMovementReverse && self.gradientMovementTransition >= 1.0 {
self.gradientMovementTransitionArguments = (CACurrentMediaTime(), 1.0, true)
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
self.animator = animator
}
animator.isPaused = false
} else {
// self.animator?.isPaused = true
}
self.setNeedsDisplay()
}
private func animateToNewShape() {
if let _ = self.transitionArguments {
self.fromPoints = self.currentPoints
self.toPoints = nil
self.transition = 0.0
self.transitionArguments = nil
}
if self.fromPoints == nil {
self.fromPoints = generateNextBlob(for: self.bounds.size)
}
if self.toPoints == nil {
self.toPoints = generateNextBlob(for: self.bounds.size)
}
let duration: Double = 1.0 / Double(minSpeed + (maxSpeed - minSpeed) * speedLevel)
self.transitionArguments = (CACurrentMediaTime(), duration)
self.lastSpeedLevel = self.speedLevel
self.speedLevel = 0
self.updateAnimations()
}
private func generateNextBlob(for size: CGSize) -> [CGPoint] {
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
return blob(pointsCount: pointsCount, randomness: randomness).map {
return CGPoint(x: size.width / 2.0 + $0.x * CGFloat(size.width), y: size.height / 2.0 + $0.y * CGFloat(size.height))
}
}
func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
let rgen = { () -> CGFloat in
let accuracy: UInt32 = 1000
let random = arc4random_uniform(accuracy)
return CGFloat(random) / CGFloat(accuracy)
}
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
let points = (0 ..< pointsCount).map { i -> CGPoint in
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
let angleRandomness: CGFloat = angle * 0.1
let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
let pointX = sin(startAngle + CGFloat(i) * randAngle)
let pointY = cos(startAngle + CGFloat(i) * randAngle)
return CGPoint(x: pointX * randPointOffset, y: pointY * randPointOffset)
}
return points
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
// var transitionState: SemanticStatusNodeTransitionDrawingState?
// var transitionFraction: CGFloat = 1.0
// var appearanceBackgroundTransitionFraction: CGFloat = 1.0
// var appearanceForegroundTransitionFraction: CGFloat = 1.0
//
// if let transitionContext = self.transitionContext {
// let timestamp = CACurrentMediaTime()
// var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
// t = min(1.0, max(0.0, t))
//
// if let _ = transitionContext.previousStateContext {
// transitionFraction = t
// }
// var foregroundTransitionFraction: CGFloat = 1.0
// if let previousContext = transitionContext.previousAppearanceContext {
// if previousContext.backgroundImage != self.appearanceContext.backgroundImage {
// appearanceBackgroundTransitionFraction = t
// }
// if previousContext.cutout != self.appearanceContext.cutout {
// appearanceForegroundTransitionFraction = t
// foregroundTransitionFraction = 1.0 - t
// }
// }
// transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext?.drawingState(transitionFraction: 1.0 - t), appearanceState: transitionContext.previousAppearanceContext?.drawingState(backgroundTransitionFraction: 1.0, foregroundTransitionFraction: foregroundTransitionFraction))
// }
return BlobNodeDrawingState(scale: self.currentScale, shape: self.currentShape, gradientTransition: self.gradientTransition, gradientMovement: self.gradientMovementTransition)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? BlobNodeDrawingState else {
return
}
if let path = parameters.shape {
var uiPath = UIBezierPath(cgPath: path)
let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
uiPath.apply(toOrigin)
uiPath.apply(CGAffineTransform(scaleX: parameters.scale, y: parameters.scale))
uiPath.apply(fromOrigin)
context.addPath(uiPath.cgPath)
context.clip()
let blue = UIColor(rgb: 0x0078ff)
let lightBlue = UIColor(rgb: 0x59c7f8)
let green = UIColor(rgb: 0x33c659)
let firstColor = lightBlue.interpolateTo(blue, fraction: parameters.gradientTransition)!
let secondColor = blue.interpolateTo(green, fraction: parameters.gradientTransition)!
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [firstColor.cgColor, secondColor.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
var center: CGPoint = CGPoint(x: bounds.size.width - 30.0, y: 50.0)
center.x -= parameters.gradientMovement * 60.0
center.y += parameters.gradientMovement * 200.0
let startRadius: CGFloat = 0.0
let endRadius: CGFloat = 260.0
context.drawRadialGradient(gradient, startCenter: center, startRadius: startRadius, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation)
}
// if let transitionAppearanceState = parameters.transitionState?.appearanceState {
// transitionAppearanceState.drawBackground(context: context, size: bounds.size)
// }
// parameters.appearanceState.drawBackground(context: context, size: bounds.size)
//
// 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: parameters.appearanceState.effectiveForegroundColor)
//
// if let transitionAppearanceState = parameters.transitionState?.appearanceState {
// transitionAppearanceState.drawForeground(context: context, size: bounds.size)
// }
// parameters.appearanceState.drawForeground(context: context, size: bounds.size)
}
}
private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0)
final class VoiceChatActionButton: HighlightTrackingButtonNode {
enum State {
enum ActiveState {
case cantSpeak
case muted
case on
}
case connecting
case active(state: ActiveState)
}
// private final class SemanticStatusNodeTransitionContext {
// let startTime: Double
// let duration: Double
// let previousStateContext: SemanticStatusNodeStateContext?
// let completion: () -> Void
//
// init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext?, previousAppearanceContext: SemanticStatusNodeAppearanceContext?, completion: @escaping () -> Void) {
// self.startTime = startTime
// self.duration = duration
// self.previousStateContext = previousStateContext
// self.previousAppearanceContext = previousAppearanceContext
// self.completion = completion
// }
// }
private let smallBlob: BlobNode
private let mediumBlob: BlobNode
private let bigBlob: BlobNode
private let iconNode: ASImageNode
private let maxLevel: CGFloat
private let glowNode: ASImageNode
let titleLabel: ImmediateTextNode
let subtitleLabel: ImmediateTextNode
private var currentParams: (size: CGSize, state: State, title: String, subtitle: String)?
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
private var audioLevel: CGFloat = 0.6
private var presentationAudioLevel: CGFloat = 0
typealias BlobRange = (min: CGFloat, max: CGFloat)
var imitateVoice = false
var previousImitateTimestamp: Double?
init(size: CGSize) {
let smallBlobRange: BlobRange = (0.6, 0.62)
let mediumBlobRange: BlobRange = (0.62, 0.87)
let bigBlobRange: BlobRange = (0.65, 1.00)
self.maxLevel = 4.0
self.glowNode = ASImageNode()
self.glowNode.alpha = 0.5
self.glowNode.image = generateImage(CGSize(width: 360.0, height: 360.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let blue = UIColor(rgb: 0x0078ff)
var locations: [CGFloat] = [0.0, 0.4, 1.0]
let colors: [CGColor] = [blue.cgColor, blue.cgColor, blue.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let center: CGPoint = CGPoint(x: 180.0, y: 180.0)
let startRadius: CGFloat = 0.0
let endRadius: CGFloat = 200.0
context.drawRadialGradient(gradient, startCenter: center, startRadius: startRadius, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation)
})
self.glowNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 360.0, height: 360.0))
self.iconNode = ASImageNode()
self.titleLabel = ImmediateTextNode()
self.subtitleLabel = ImmediateTextNode()
self.smallBlob = BlobNode(
size: size,
pointsCount: 8,
minRandomness: 0.1,
maxRandomness: 0.1,
minSpeed: 0.2,
maxSpeed: 0.6,
minScale: smallBlobRange.min,
maxScale: smallBlobRange.max,
scaleSpeed: 0.2,
isCircle: true
)
self.smallBlob.alpha = 1.0
self.mediumBlob = BlobNode(
size: size,
pointsCount: 8,
minRandomness: 1,
maxRandomness: 1,
minSpeed: 1.5,
maxSpeed: 7,
minScale: mediumBlobRange.min,
maxScale: mediumBlobRange.max,
scaleSpeed: 0.2,
isCircle: false
)
self.mediumBlob.alpha = 0.65
self.bigBlob = BlobNode(
size: size,
pointsCount: 8,
minRandomness: 1,
maxRandomness: 1,
minSpeed: 1.5,
maxSpeed: 7,
minScale: bigBlobRange.min,
maxScale: bigBlobRange.max,
scaleSpeed: 0.2,
isCircle: false
)
self.bigBlob.alpha = 0.45
super.init()
self.addSubnode(self.glowNode)
self.addSubnode(self.titleLabel)
self.addSubnode(self.subtitleLabel)
self.addSubnode(self.bigBlob)
self.addSubnode(self.mediumBlob)
self.addSubnode(self.smallBlob)
self.iconNode.image = UIImage(bundleImageName: "Call/VoiceChatMicOff")
self.addSubnode(self.iconNode)
self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
guard let strongSelf = self else { return }
if strongSelf.imitateVoice {
let timestamp = CACurrentMediaTime()
if let previousTimestamp = strongSelf.previousImitateTimestamp {
if timestamp - previousTimestamp > 0.05 {
strongSelf.previousImitateTimestamp = timestamp
strongSelf.updateLevel(CGFloat(Float.random(in: 0.3 ..< 4.0)))
}
} else {
strongSelf.previousImitateTimestamp = timestamp
}
} else {
strongSelf.previousImitateTimestamp = nil
strongSelf.audioLevel = 0.6
}
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
strongSelf.smallBlob.level = strongSelf.presentationAudioLevel
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
}
}
private(set) var isAnimating = false
func startAnimating() {
guard !isAnimating else { return }
isAnimating = true
mediumBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false)
bigBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false)
updateBlobsState()
displayLinkAnimator?.isPaused = false
}
func stopAnimating() {
guard isAnimating else { return }
isAnimating = false
mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
updateBlobsState()
displayLinkAnimator?.isPaused = true
}
private func updateBlobsState() {
if self.isAnimating {
if smallBlob.frame.size != .zero {
smallBlob.startAnimating()
mediumBlob.startAnimating()
bigBlob.startAnimating()
}
} else {
smallBlob.stopAnimating()
mediumBlob.stopAnimating()
bigBlob.stopAnimating()
}
}
func updateLevel(_ level: CGFloat) {
let normalizedLevel = min(1, max(level / maxLevel, 0))
smallBlob.updateSpeedLevel(to: normalizedLevel)
mediumBlob.updateSpeedLevel(to: normalizedLevel)
bigBlob.updateSpeedLevel(to: normalizedLevel)
audioLevel = normalizedLevel
}
func update(size: CGSize, state: State, title: String, subtitle: String, animated: Bool = false) {
let updatedTitle = self.currentParams?.title != title
let updatedSubtitle = self.currentParams?.subtitle != subtitle
self.currentParams = (size, state, title, subtitle)
self.smallBlob.frame = CGRect(origin: CGPoint(), size: size)
self.mediumBlob.frame = CGRect(origin: CGPoint(), size: size)
self.bigBlob.frame = CGRect(origin: CGPoint(), size: size)
self.updateBlobsState()
self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white)
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white)
if case let .active(state) = state {
if state == .muted {
self.smallBlob.loop = true
self.mediumBlob.loop = true
self.bigBlob.loop = true
self.imitateVoice = false
} else {
self.smallBlob.loop = false
self.mediumBlob.loop = false
self.bigBlob.loop = false
self.imitateVoice = true
}
}
if animated {
if let snapshotView = self.titleLabel.view.snapshotContentTree(), updatedTitle {
self.titleLabel.view.insertSubview(snapshotView, belowSubview: self.titleLabel.view)
snapshotView.frame = self.titleLabel.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let snapshotView = self.subtitleLabel.view.snapshotContentTree(), updatedSubtitle {
self.subtitleLabel.view.insertSubview(snapshotView, belowSubview: self.subtitleLabel.view)
snapshotView.frame = self.subtitleLabel.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
let titleSize = self.titleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height + 16.0 - totalHeight / 2.0) - 20.0), size: titleSize)
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
self.glowNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
}
// func updateLayout(size: CGSize, isOn: Bool) {
// if self.validSize != size {
// self.validSize = size
//
// self.backgroundNode.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1C1C1E))
// }
// if self.isOn != isOn {
// self.isOn = isOn
// self.foregroundNode.image = UIImage(bundleImageName: isOn ? "Call/VoiceChatMicOn" : "Call/VoiceChatMicOff")
// }
// self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
//
// if let image = self.foregroundNode.image {
// self.foregroundNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
// }
// }
}

View File

@ -11,11 +11,12 @@ import AccountContext
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
import ItemListPeerItem
import MergeLists import MergeLists
import ItemListUI import ItemListUI
import AppBundle import AppBundle
import RadialStatusNode import ContextUI
import ShareController
import DeleteChatPeerActionSheetItem
private final class VoiceChatControllerTitleView: UIView { private final class VoiceChatControllerTitleView: UIView {
private var theme: PresentationTheme private var theme: PresentationTheme
@ -80,41 +81,6 @@ private final class VoiceChatControllerTitleView: UIView {
} }
} }
private final class VoiceChatActionButton: HighlightTrackingButtonNode {
private let backgroundNode: ASImageNode
private let foregroundNode: ASImageNode
private var validSize: CGSize?
private var isOn: Bool?
init() {
self.backgroundNode = ASImageNode()
self.foregroundNode = ASImageNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.foregroundNode)
}
func updateLayout(size: CGSize, isOn: Bool) {
if self.validSize != size {
self.validSize = size
self.backgroundNode.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1C1C1E))
}
if self.isOn != isOn {
self.isOn = isOn
self.foregroundNode.image = UIImage(bundleImageName: isOn ? "Call/VoiceChatMicOn" : "Call/VoiceChatMicOff")
}
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
if let image = self.foregroundNode.image {
self.foregroundNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
}
}
public final class VoiceChatController: ViewController { public final class VoiceChatController: ViewController {
private final class Node: ViewControllerTracingNode { private final class Node: ViewControllerTracingNode {
private struct ListTransition { private struct ListTransition {
@ -127,7 +93,11 @@ public final class VoiceChatController: ViewController {
} }
private final class Interaction { private final class Interaction {
let peerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void
init(peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) {
self.peerContextAction = peerContextAction
}
} }
private struct PeerEntry: Comparable, Identifiable { private struct PeerEntry: Comparable, Identifiable {
@ -152,32 +122,26 @@ public final class VoiceChatController: ViewController {
return lhs.participant.peer.id < rhs.participant.peer.id return lhs.participant.peer.id < rhs.participant.peer.id
} }
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListViewItem { func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
let peer = self.participant.peer let peer = self.participant.peer
let text: ItemListPeerItemText let text: VoiceChatParticipantItem.ParticipantText
switch self.state { switch self.state {
case .inactive: case .inactive:
text = .presence text = .presence
case .listening: case .listening:
//TODO:localize text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
text = .text("listening", .accent)
case .speaking: case .speaking:
//TODO:localize text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
text = .text("speaking", .constructive)
} }
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: context, peer: peer, height: .peerList, presence: self.participant.presences[self.participant.peer.id], text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.participant.presences[self.participant.peer.id], text: text, enabled: true, action: nil, contextAction: { node, gesture in
//arguments.deleteIncludePeer(peer.peerId) interaction.peerContextAction(peer, node, gesture)
})]), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in })
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
}, removePeer: { id in
//arguments.deleteIncludePeer(id)
}, noInsets: true)
} }
} }
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListTransition { private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
@ -194,12 +158,12 @@ public final class VoiceChatController: ViewController {
private var presentationData: PresentationData private var presentationData: PresentationData
private var darkTheme: PresentationTheme private var darkTheme: PresentationTheme
private let optionsButton: VoiceChatOptionsButton
private let contentContainer: ASDisplayNode private let contentContainer: ASDisplayNode
private let listNode: ListView private let listNode: ListView
private let audioOutputNode: CallControllerButtonItemNode private let audioOutputNode: CallControllerButtonItemNode
private let leaveNode: CallControllerButtonItemNode private let leaveNode: CallControllerButtonItemNode
private let actionButton: VoiceChatActionButton private let actionButton: VoiceChatActionButton
private let radialStatus: RadialStatusNode
private let statusLabel: ImmediateTextNode private let statusLabel: ImmediateTextNode
private var enqueuedTransitions: [ListTransition] = [] private var enqueuedTransitions: [ListTransition] = []
@ -236,7 +200,9 @@ public final class VoiceChatController: ViewController {
self.call = call self.call = call
self.presentationData = sharedContext.currentPresentationData.with { $0 } self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.darkTheme = defaultDarkPresentationTheme self.darkTheme = defaultDarkColorPresentationTheme
self.optionsButton = VoiceChatOptionsButton()
self.contentContainer = ASDisplayNode() self.contentContainer = ASDisplayNode()
@ -248,23 +214,63 @@ public final class VoiceChatController: ViewController {
self.audioOutputNode = CallControllerButtonItemNode() self.audioOutputNode = CallControllerButtonItemNode()
self.leaveNode = CallControllerButtonItemNode() self.leaveNode = CallControllerButtonItemNode()
self.actionButton = VoiceChatActionButton() self.actionButton = VoiceChatActionButton(size: CGSize(width: 244.0, height: 244.0))
self.statusLabel = ImmediateTextNode() self.statusLabel = ImmediateTextNode()
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
super.init() super.init()
self.itemInteraction = Interaction() self.itemInteraction = Interaction(peerContextAction: { [weak self] peer, sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme))
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: peer, chatPeer: peer, action: .removeFromGroup, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})))
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)
})
self.backgroundColor = .black self.backgroundColor = .black
self.contentContainer.addSubnode(self.actionButton)
self.contentContainer.addSubnode(self.listNode) self.contentContainer.addSubnode(self.listNode)
self.contentContainer.addSubnode(self.audioOutputNode) self.contentContainer.addSubnode(self.audioOutputNode)
self.contentContainer.addSubnode(self.leaveNode) self.contentContainer.addSubnode(self.leaveNode)
self.contentContainer.addSubnode(self.actionButton)
self.contentContainer.addSubnode(self.statusLabel) self.contentContainer.addSubnode(self.statusLabel)
self.contentContainer.addSubnode(self.radialStatus)
self.addSubnode(self.contentContainer) self.addSubnode(self.contentContainer)
@ -360,6 +366,48 @@ public final class VoiceChatController: ViewController {
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside) self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
self.optionsButton.contextAction = { [weak self, weak optionsButton] sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller, let strongOptionsButton = optionsButton else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { _ in return nil}, action: { _, f in
f(.dismissWithoutContent)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let strongSelf = self {
let shareController = ShareController(context: strongSelf.context, subject: .url("url"), forcedTheme: strongSelf.darkTheme)
strongSelf.controller?.present(shareController, in: .window(.root))
}
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: strongOptionsButton.extractedContainerNode)), items: .single(items), reactionItems: [], gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)
}
let optionsButtonItem = UIBarButtonItem(customDisplayNode: self.optionsButton)!
optionsButtonItem.target = self
optionsButtonItem.action = #selector(self.rightNavigationButtonAction)
self.controller?.navigationItem.setRightBarButton(optionsButtonItem, animated: false)
} }
deinit { deinit {
@ -372,6 +420,10 @@ public final class VoiceChatController: ViewController {
self.memberStatesDisposable?.dispose() self.memberStatesDisposable?.dispose()
} }
@objc private func rightNavigationButtonAction() {
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
}
@objc private func leavePressed() { @objc private func leavePressed() {
self.leaveDisposable.set((self.call.leave() self.leaveDisposable.set((self.call.leave()
|> deliverOnMainQueue).start(completed: { [weak self] in |> deliverOnMainQueue).start(completed: { [weak self] in
@ -435,13 +487,6 @@ public final class VoiceChatController: ViewController {
})) }))
} }
if hasMute {
items.append(CallRouteActionSheetItem(title: self.presentationData.strings.Call_AudioRouteMute, icon: generateScaledImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false), selected: self.callState?.isMuted ?? true, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.call.toggleIsMuted()
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated() actionSheet?.dismissAnimated()
@ -458,10 +503,11 @@ public final class VoiceChatController: ViewController {
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
let bottomAreaHeight: CGFloat = 302.0 let bottomAreaHeight: CGFloat = 333.0
let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0) let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0)
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y))) // let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y)))
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: 168.0))
transition.updateFrame(node: self.listNode, frame: listFrame) transition.updateFrame(node: self.listNode, frame: listFrame)
@ -471,7 +517,7 @@ public final class VoiceChatController: ViewController {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let sideButtonSize = CGSize(width: 60.0, height: 60.0) let sideButtonSize = CGSize(width: 60.0, height: 60.0)
let centralButtonSize = CGSize(width: 144.0, height: 144.0) let centralButtonSize = CGSize(width: 244.0, height: 244.0)
let sideButtonInset: CGFloat = 27.0 let sideButtonInset: CGFloat = 27.0
var audioMode: CallControllerButtonsSpeakerMode = .none var audioMode: CallControllerButtonsSpeakerMode = .none
@ -507,7 +553,7 @@ public final class VoiceChatController: ViewController {
soundImage = .speaker soundImage = .speaker
case .speaker: case .speaker:
soundImage = .speaker soundImage = .speaker
soundAppearance = .blurred(isFilled: true) soundAppearance = .blurred(isFilled: false)
case .headphones: case .headphones:
soundImage = .bluetooth soundImage = .bluetooth
case let .bluetooth(type): case let .bluetooth(type):
@ -521,11 +567,9 @@ public final class VoiceChatController: ViewController {
} }
} }
//TODO:localize self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .immediate)
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: "audio", transition: .immediate)
//TODO:localize self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: "leave", transition: .immediate)
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)) transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonInset - sideButtonSize.width, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)) transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonInset - sideButtonSize.width, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
@ -533,32 +577,35 @@ public final class VoiceChatController: ViewController {
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize) let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
var isMicOn = false var isMicOn = false
let actionButtonState: VoiceChatActionButton.State
let actionButtonTitle: String
let actionButtonSubtitle: String
if let callState = callState { if let callState = callState {
isMicOn = !callState.isMuted isMicOn = !callState.isMuted
switch callState.networkState { // switch callState.networkState {
case .connecting: // case .connecting:
self.radialStatus.isHidden = false // actionButtonState = .connecting
// actionButtonTitle = "Connecting..."
self.statusLabel.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(17.0), textColor: .white) // actionButtonSubtitle = ""
case .connected: // case .connected:
self.radialStatus.isHidden = true actionButtonState = .active(state: isMicOn ? .on : .muted)
if isMicOn { if isMicOn {
self.statusLabel.attributedText = NSAttributedString(string: "You're Live", font: Font.regular(17.0), textColor: .white) actionButtonTitle = self.presentationData.strings.VoiceChat_Live
actionButtonSubtitle = ""
} else { } else {
self.statusLabel.attributedText = NSAttributedString(string: "Unmute", font: Font.regular(17.0), textColor: .white) actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
actionButtonSubtitle = self.presentationData.strings.VoiceChat_UnmuteHelp
} }
// }
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
} }
let statusSize = self.statusLabel.updateLayout(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude)) self.actionButton.update(size: centralButtonSize, state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, animated: true)
self.statusLabel.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: actionButtonFrame.maxY + 12.0), size: statusSize)
self.radialStatus.transitionToState(.progress(color: UIColor(rgb: 0x00ACFF), lineWidth: 3.3, value: nil, cancelEnabled: false), animated: false)
self.radialStatus.frame = actionButtonFrame.insetBy(dx: -3.3, dy: -3.3)
}
self.actionButton.updateLayout(size: centralButtonSize, isOn: isMicOn)
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
if isFirstTime { if isFirstTime {
@ -574,10 +621,18 @@ public final class VoiceChatController: ViewController {
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.actionButton.startAnimating()
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} }
@ -674,8 +729,7 @@ public final class VoiceChatController: ViewController {
self.currentEntries = entries self.currentEntries = entries
let presentationData = ItemListPresentationData(theme: self.darkTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings) let presentationData = self.presentationData.withUpdated(theme: self.darkTheme)
let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!)
self.enqueueTransition(transition) self.enqueueTransition(transition)
} }
@ -710,7 +764,7 @@ public final class VoiceChatController: ViewController {
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: "Chat", target: self, action: #selector(self.closePressed)) let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.VoiceChat_BackTitle, target: self, action: #selector(self.closePressed))
self.navigationItem.leftBarButtonItem = backItem self.navigationItem.leftBarButtonItem = backItem
self.statusBar.statusBarStyle = .White self.statusBar.statusBarStyle = .White
@ -774,3 +828,24 @@ public final class VoiceChatController: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
} }
} }
private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = true
let ignoreContentTouches: Bool = true
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
final class VoiceChatOptionsButton: HighlightableButtonNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init()
self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.iconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubnode(self.containerNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.iconNode.image = generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(x: 6.0, y: 12.0, width: 4.0, height: 4.0))
context.fillEllipse(in: CGRect(x: 12.0, y: 12.0, width: 4.0, height: 4.0))
context.fillEllipse(in: CGRect(x: 18.0, y: 12.0, width: 4.0, height: 4.0))
})
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
self.extractedContainerNode.frame = self.containerNode.bounds
self.iconNode.frame = self.containerNode.bounds
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 28.0, height: 28.0)
}
func onLayout() {
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import SyncCore
import TelegramUIPreferences import TelegramUIPreferences
public let defaultDarkPresentationTheme = makeDefaultDarkPresentationTheme(preview: false) public let defaultDarkPresentationTheme = makeDefaultDarkPresentationTheme(preview: false)
public let defaultDarkColorPresentationTheme = customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: UIColor(rgb: 0x007aff), backgroundColors: nil, bubbleColors: nil, wallpaper: nil)
public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme { public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme {
if (theme.referenceTheme != .night) { if (theme.referenceTheme != .night) {

View File

@ -90,6 +90,10 @@ public final class PresentationData: Equatable {
self.largeEmoji = largeEmoji self.largeEmoji = largeEmoji
} }
public func withUpdated(theme: PresentationTheme) -> PresentationData {
return PresentationData(strings: self.strings, theme: theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji)
}
public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool {
return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations && lhs.largeEmoji == rhs.largeEmoji return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations && lhs.largeEmoji == rhs.largeEmoji
} }

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_menucheck.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1 @@
{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}

View File

@ -21,8 +21,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
} }
} }
var tapped: (() -> Void)?
override init() { override init() {
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.avatarNode = AvatarNode(font: normalFont) self.avatarNode = AvatarNode(font: normalFont)
@ -41,9 +39,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0) self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0)
self.avatarNode.frame = self.containerNode.bounds self.avatarNode.frame = self.containerNode.bounds
/*self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0))
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0))*/
} }
override func didLoad() { override func didLoad() {
@ -51,19 +46,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
self.view.isOpaque = false self.view.isOpaque = false
} }
@objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.tapped?()
default:
break
}
}
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 37.0, height: 37.0) return CGSize(width: 37.0, height: 37.0)
} }

View File

@ -5979,11 +5979,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture) let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController) strongSelf.presentInGlobalOverlay(contextController)
}, joinGroupCall: { [weak self] activeCall in }, joinGroupCall: { [weak self] activeCall in
}, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self {
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
}
}, joinGroupCall: { [weak self] messageId in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return return
} }
@ -6011,6 +6006,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
} }
} }
}, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self {
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
}
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
do { do {

View File

@ -207,7 +207,7 @@ final class ChatPanelInterfaceInteraction {
scrollToTop: @escaping () -> Void, scrollToTop: @escaping () -> Void,
viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void, viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void,
activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void, activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void,
joinGroupCall: @escaping (MessageId) -> Void, joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void,
editMessageMedia: @escaping (MessageId, Bool) -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void,
statuses: ChatPanelInterfaceInteractionStatuses? statuses: ChatPanelInterfaceInteractionStatuses?
) { ) {

View File

@ -132,8 +132,8 @@ final class ChatRecentActionsController: TelegramBaseController {
}, scrollToTop: { }, scrollToTop: {
}, viewReplies: { _, _ in }, viewReplies: { _, _ in
}, activatePinnedListPreview: { _, _ in }, activatePinnedListPreview: { _, _ in
}, editMessageMedia: { _, _ in
}, joinGroupCall: { _ in }, joinGroupCall: { _ in
}, editMessageMedia: { _, _ in
}, statuses: nil) }, statuses: nil)
self.navigationItem.titleView = self.titleView self.navigationItem.titleView = self.titleView

View File

@ -438,8 +438,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, scrollToTop: { }, scrollToTop: {
}, viewReplies: { _, _ in }, viewReplies: { _, _ in
}, activatePinnedListPreview: { _, _ in }, activatePinnedListPreview: { _, _ in
}, editMessageMedia: { _, _ in
}, joinGroupCall: { _ in }, joinGroupCall: { _ in
}, editMessageMedia: { _, _ in
}, statuses: nil) }, statuses: nil)
self.selectionPanel.interfaceInteraction = interfaceInteraction self.selectionPanel.interfaceInteraction = interfaceInteraction