mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chats UI improvements
This commit is contained in:
parent
e87908a532
commit
a362cab636
@ -5895,3 +5895,42 @@ Sorry for the inconvenience.";
|
||||
"Conversation.EditingPhotoPanelTitle" = "Edit Photo";
|
||||
|
||||
"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";
|
||||
|
@ -14,6 +14,7 @@ public enum DeleteChatPeerAction {
|
||||
case clearHistory
|
||||
case clearCache
|
||||
case clearCacheSuggestion
|
||||
case removeFromGroup
|
||||
}
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 26.0)
|
||||
@ -115,6 +116,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
}
|
||||
case .clearHistory:
|
||||
text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
case .removeFromGroup:
|
||||
text = strings.VoiceChat_RemovePeerConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -283,6 +283,7 @@ public final class ShareController: ViewController {
|
||||
private var currentAccount: Account
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private let forcedTheme: PresentationTheme?
|
||||
|
||||
private let externalShare: Bool
|
||||
private let immediateExternalShare: Bool
|
||||
@ -302,11 +303,11 @@ public final class ShareController: ViewController {
|
||||
|
||||
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) {
|
||||
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)
|
||||
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, 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.currentContext = currentContext
|
||||
self.currentAccount = currentContext.account
|
||||
@ -318,8 +319,12 @@ public final class ShareController: ViewController {
|
||||
self.immediatePeerId = immediatePeerId
|
||||
self.openStats = openStats
|
||||
self.shares = shares
|
||||
self.forcedTheme = forcedTheme
|
||||
|
||||
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
||||
if let forcedTheme = self.forcedTheme {
|
||||
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
|
||||
}
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -441,7 +446,7 @@ public final class ShareController: ViewController {
|
||||
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))
|
||||
}, 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?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
self?.dismissed?(shared)
|
||||
|
@ -29,6 +29,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
private let sharedContext: SharedAccountContext
|
||||
private var context: AccountContext?
|
||||
private var presentationData: PresentationData
|
||||
private let forcedTheme: PresentationTheme?
|
||||
private let externalShare: Bool
|
||||
private let immediateExternalShare: Bool
|
||||
private var immediatePeerId: PeerId?
|
||||
@ -80,9 +81,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
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.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.forcedTheme = forcedTheme
|
||||
self.externalShare = externalShare
|
||||
self.immediateExternalShare = immediateExternalShare
|
||||
self.immediatePeerId = immediatePeerId
|
||||
@ -94,6 +96,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.defaultAction = defaultAction
|
||||
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 highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
|
||||
|
||||
@ -260,6 +266,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
return
|
||||
}
|
||||
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 highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
|
||||
|
@ -27,6 +27,11 @@ swift_library(
|
||||
"//submodules/ItemListPeerItem:ItemListPeerItem",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/ShareController:ShareController",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
|
||||
"//submodules/AnimationUI:AnimationUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -62,7 +62,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
private let contentNode: ASImageNode
|
||||
private let overlayHighlightNode: ASImageNode
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private let textNode: ImmediateTextNode
|
||||
let textNode: ImmediateTextNode
|
||||
|
||||
private let largeButtonSize: CGFloat = 72.0
|
||||
|
||||
@ -199,7 +199,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
case .green:
|
||||
fillColor = UIColor(rgb: 0x74db58)
|
||||
case .redDimmed:
|
||||
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.3)
|
||||
fillColor = UIColor(rgb: 0x4d120e)
|
||||
case .greenDimmed:
|
||||
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3)
|
||||
case .grayDimmed:
|
||||
|
744
submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift
Normal file
744
submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift
Normal 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)
|
||||
// }
|
||||
// }
|
||||
}
|
@ -11,11 +11,12 @@ import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import ItemListPeerItem
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import AppBundle
|
||||
import RadialStatusNode
|
||||
import ContextUI
|
||||
import ShareController
|
||||
import DeleteChatPeerActionSheetItem
|
||||
|
||||
private final class VoiceChatControllerTitleView: UIView {
|
||||
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 {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
private struct ListTransition {
|
||||
@ -127,7 +93,11 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -152,32 +122,26 @@ public final class VoiceChatController: ViewController {
|
||||
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 text: ItemListPeerItemText
|
||||
let text: VoiceChatParticipantItem.ParticipantText
|
||||
switch self.state {
|
||||
case .inactive:
|
||||
text = .presence
|
||||
case .listening:
|
||||
//TODO:localize
|
||||
text = .text("listening", .accent)
|
||||
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
|
||||
case .speaking:
|
||||
//TODO:localize
|
||||
text = .text("speaking", .constructive)
|
||||
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .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: {
|
||||
//arguments.deleteIncludePeer(peer.peerId)
|
||||
})]), 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)
|
||||
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
|
||||
interaction.peerContextAction(peer, node, gesture)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
@ -194,12 +158,12 @@ public final class VoiceChatController: ViewController {
|
||||
private var presentationData: PresentationData
|
||||
private var darkTheme: PresentationTheme
|
||||
|
||||
private let optionsButton: VoiceChatOptionsButton
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
private let audioOutputNode: CallControllerButtonItemNode
|
||||
private let leaveNode: CallControllerButtonItemNode
|
||||
private let actionButton: VoiceChatActionButton
|
||||
private let radialStatus: RadialStatusNode
|
||||
private let statusLabel: ImmediateTextNode
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
@ -236,7 +200,9 @@ public final class VoiceChatController: ViewController {
|
||||
self.call = call
|
||||
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.darkTheme = defaultDarkPresentationTheme
|
||||
self.darkTheme = defaultDarkColorPresentationTheme
|
||||
|
||||
self.optionsButton = VoiceChatOptionsButton()
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
|
||||
@ -248,23 +214,63 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.audioOutputNode = CallControllerButtonItemNode()
|
||||
self.leaveNode = CallControllerButtonItemNode()
|
||||
self.actionButton = VoiceChatActionButton()
|
||||
self.actionButton = VoiceChatActionButton(size: CGSize(width: 244.0, height: 244.0))
|
||||
self.statusLabel = ImmediateTextNode()
|
||||
|
||||
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
|
||||
|
||||
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.contentContainer.addSubnode(self.actionButton)
|
||||
self.contentContainer.addSubnode(self.listNode)
|
||||
self.contentContainer.addSubnode(self.audioOutputNode)
|
||||
self.contentContainer.addSubnode(self.leaveNode)
|
||||
self.contentContainer.addSubnode(self.actionButton)
|
||||
self.contentContainer.addSubnode(self.statusLabel)
|
||||
self.contentContainer.addSubnode(self.radialStatus)
|
||||
|
||||
self.addSubnode(self.contentContainer)
|
||||
|
||||
@ -360,6 +366,48 @@ public final class VoiceChatController: ViewController {
|
||||
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), 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 {
|
||||
@ -372,6 +420,10 @@ public final class VoiceChatController: ViewController {
|
||||
self.memberStatesDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func rightNavigationButtonAction() {
|
||||
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
|
||||
}
|
||||
|
||||
@objc private func leavePressed() {
|
||||
self.leaveDisposable.set((self.call.leave()
|
||||
|> 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: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
@ -458,10 +503,11 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
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 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)
|
||||
|
||||
@ -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 })
|
||||
|
||||
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
|
||||
|
||||
var audioMode: CallControllerButtonsSpeakerMode = .none
|
||||
@ -507,7 +553,7 @@ public final class VoiceChatController: ViewController {
|
||||
soundImage = .speaker
|
||||
case .speaker:
|
||||
soundImage = .speaker
|
||||
soundAppearance = .blurred(isFilled: true)
|
||||
soundAppearance = .blurred(isFilled: false)
|
||||
case .headphones:
|
||||
soundImage = .bluetooth
|
||||
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: "audio", transition: .immediate)
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .immediate)
|
||||
|
||||
//TODO:localize
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: "leave", transition: .immediate)
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: self.presentationData.strings.VoiceChat_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.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)
|
||||
|
||||
var isMicOn = false
|
||||
|
||||
let actionButtonState: VoiceChatActionButton.State
|
||||
let actionButtonTitle: String
|
||||
let actionButtonSubtitle: String
|
||||
if let callState = callState {
|
||||
isMicOn = !callState.isMuted
|
||||
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
self.radialStatus.isHidden = false
|
||||
|
||||
self.statusLabel.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(17.0), textColor: .white)
|
||||
case .connected:
|
||||
self.radialStatus.isHidden = true
|
||||
|
||||
// switch callState.networkState {
|
||||
// case .connecting:
|
||||
// actionButtonState = .connecting
|
||||
// actionButtonTitle = "Connecting..."
|
||||
// actionButtonSubtitle = ""
|
||||
// case .connected:
|
||||
actionButtonState = .active(state: isMicOn ? .on : .muted)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
let statusSize = self.statusLabel.updateLayout(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude))
|
||||
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)
|
||||
// }
|
||||
} else {
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
}
|
||||
|
||||
self.actionButton.updateLayout(size: centralButtonSize, isOn: isMicOn)
|
||||
self.actionButton.update(size: centralButtonSize, state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, animated: true)
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
|
||||
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.actionButton.startAnimating()
|
||||
|
||||
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.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)
|
||||
}
|
||||
|
||||
@ -674,8 +729,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
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!)
|
||||
self.enqueueTransition(transition)
|
||||
}
|
||||
@ -709,8 +763,8 @@ public final class VoiceChatController: ViewController {
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
|
||||
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.statusBar.statusBarStyle = .White
|
||||
@ -774,3 +828,24 @@ public final class VoiceChatController: ViewController {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
1004
submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift
Normal file
1004
submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import SyncCore
|
||||
import TelegramUIPreferences
|
||||
|
||||
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 {
|
||||
if (theme.referenceTheme != .night) {
|
||||
|
@ -90,6 +90,10 @@ public final class PresentationData: Equatable {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_menucheck.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/ic_menucheck.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/ic_menucheck.pdf
vendored
Normal file
Binary file not shown.
@ -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":[]}
|
Binary file not shown.
@ -20,9 +20,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
|
||||
|
||||
override init() {
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
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.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() {
|
||||
@ -51,19 +46,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
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 {
|
||||
return CGSize(width: 37.0, height: 37.0)
|
||||
}
|
||||
|
@ -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)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}, 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 {
|
||||
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()))
|
||||
|
||||
do {
|
||||
|
@ -207,7 +207,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
scrollToTop: @escaping () -> Void,
|
||||
viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void,
|
||||
activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void,
|
||||
joinGroupCall: @escaping (MessageId) -> Void,
|
||||
joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void,
|
||||
editMessageMedia: @escaping (MessageId, Bool) -> Void,
|
||||
statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
) {
|
||||
|
@ -132,8 +132,8 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, scrollToTop: {
|
||||
}, viewReplies: { _, _ in
|
||||
}, activatePinnedListPreview: { _, _ in
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, joinGroupCall: { _ in
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, statuses: nil)
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
@ -438,8 +438,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
}, scrollToTop: {
|
||||
}, viewReplies: { _, _ in
|
||||
}, activatePinnedListPreview: { _, _ in
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, joinGroupCall: { _ in
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, statuses: nil)
|
||||
|
||||
self.selectionPanel.interfaceInteraction = interfaceInteraction
|
||||
|
Loading…
x
Reference in New Issue
Block a user