Voice Chat volume UI

This commit is contained in:
Ilya Laktyushin 2020-12-29 22:10:33 +04:00
parent ec5b568d15
commit a706c210de
18 changed files with 6301 additions and 5630 deletions

View File

@ -5783,3 +5783,21 @@ Sorry for the inconvenience.";
"CallList.RecentCallsHeader" = "RECENT CALLS";
"VoiceChat.PeerJoinedText" = "%@ joined the voice chat";
"VoiceChat.Rec" = "REC";
"VoiceChat.StartRecording" = "Start Recording";
"VoiceChat.StopRecording" = "Stop Recording";
"VoiceChat.StartRecordingTitle" = "Start Recording";
"VoiceChat.StartRecordingText" = "Do you want to start recording this chat and save the result into an audio file?\n\nOther members will see that the chat is being recorded.";
"VoiceChat.StartRecordingStart" = "Start";
"VoiceChat.RecordingStarted" = "Voice chat recording started";
"VoiceChat.RecordingInProgress" = "Voice chat is being recorded";
"VoiceChat.StartRecordingTitle" = "Stop Recording?";
"VoiceChat.StartRecordingStop" = "Stop";
"VoiceChat.StatusMutedForYou" = "muted for you";
"VoiceChat.StatusMutedYou" = "put you on mute";

View File

@ -36,12 +36,15 @@ private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecogn
private enum ContextItemNode {
case action(ContextActionNode)
case custom(ContextMenuCustomNode)
case itemSeparator(ASDisplayNode)
case separator(ASDisplayNode)
}
private final class InnerActionsContainerNode: ASDisplayNode {
private let blurBackground: Bool
private let presentationData: PresentationData
private let containerNode: ASDisplayNode
private var effectView: UIVisualEffectView?
private var itemNodes: [ContextItemNode]
private let feedbackTap: () -> Void
@ -69,6 +72,12 @@ private final class InnerActionsContainerNode: ASDisplayNode {
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
self.presentationData = presentationData
self.feedbackTap = feedbackTap
self.blurBackground = blurBackground
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.containerNode.cornerRadius = 14.0
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
var itemNodes: [ContextItemNode] = []
for i in 0 ..< items.count {
@ -80,6 +89,13 @@ private final class InnerActionsContainerNode: ASDisplayNode {
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
itemNodes.append(.itemSeparator(separatorNode))
}
case let .custom(item):
itemNodes.append(.custom(item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected)))
if i != items.count - 1, case .action = items[i + 1] {
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
itemNodes.append(.itemSeparator(separatorNode))
}
case .separator:
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
@ -91,23 +107,19 @@ private final class InnerActionsContainerNode: ASDisplayNode {
super.init()
self.clipsToBounds = true
self.cornerRadius = 14.0
self.backgroundColor = presentationData.theme.contextMenu.backgroundColor
if !blurBackground {
self.backgroundColor = self.backgroundColor?.withAlphaComponent(1.0)
}
self.addSubnode(self.containerNode)
self.itemNodes.forEach({ itemNode in
switch itemNode {
case let .action(actionNode):
actionNode.isUserInteractionEnabled = false
self.addSubnode(actionNode)
self.containerNode.addSubnode(actionNode)
case let .custom(itemNode):
self.containerNode.addSubnode(itemNode)
case let .itemSeparator(separatorNode):
self.addSubnode(separatorNode)
self.containerNode.addSubnode(separatorNode)
case let .separator(separatorNode):
self.addSubnode(separatorNode)
self.containerNode.addSubnode(separatorNode)
}
})
@ -145,6 +157,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
var minActionsWidth: CGFloat = 250.0
switch widthClass {
case .compact:
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
@ -167,7 +180,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
}
self.effectView = effectView
self.view.insertSubview(effectView, at: 0)
self.containerNode.view.insertSubview(effectView, at: 0)
}
}
minActionsWidth = min(minActionsWidth, constrainedWidth)
@ -199,6 +212,11 @@ private final class InnerActionsContainerNode: ASDisplayNode {
maxWidth = max(maxWidth, minSize.width)
heightsAndCompletions.append((minSize.height, complete))
contentHeight += minSize.height
case let .custom(itemNode):
let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth)
maxWidth = max(maxWidth, minSize.width)
heightsAndCompletions.append((minSize.height, complete))
contentHeight += minSize.height
case .itemSeparator:
heightsAndCompletions.append(nil)
contentHeight += UIScreenPixel
@ -220,6 +238,13 @@ private final class InnerActionsContainerNode: ASDisplayNode {
itemCompletion(itemSize, transition)
verticalOffset += itemHeight
}
case let .custom(itemNode):
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
let itemSize = CGSize(width: maxWidth, height: itemHeight)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
itemCompletion(itemSize, transition)
verticalOffset += itemHeight
}
case let .itemSeparator(separatorNode):
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: UIScreenPixel)))
verticalOffset += UIScreenPixel
@ -230,8 +255,11 @@ private final class InnerActionsContainerNode: ASDisplayNode {
}
let size = CGSize(width: maxWidth, height: verticalOffset)
let bounds = CGRect(origin: CGPoint(), size: size)
transition.updateFrame(node: self.containerNode, frame: bounds)
if let effectView = self.effectView {
transition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(view: effectView, frame: bounds)
}
return size
}
@ -241,6 +269,8 @@ private final class InnerActionsContainerNode: ASDisplayNode {
switch itemNode {
case let .action(action):
action.updateTheme(presentationData: presentationData)
case let .custom(item):
item.updateTheme(presentationData: presentationData)
case let .separator(separator):
separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
case let .itemSeparator(itemSeparator):
@ -248,7 +278,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
}
}
self.backgroundColor = presentationData.theme.contextMenu.backgroundColor
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
}
func actionNode(at point: CGPoint) -> ContextActionNode? {
@ -393,6 +423,8 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
}
final class ContextActionsContainerNode: ASDisplayNode {
private let blurBackground: Bool
private let shadowNode: ASImageNode
private let actionsNode: InnerActionsContainerNode
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
private let scrollNode: ASScrollNode
@ -406,6 +438,14 @@ final class ContextActionsContainerNode: ASDisplayNode {
}
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) {
self.blurBackground = blurBackground
self.shadowNode = ASImageNode()
self.shadowNode.displaysAsynchronously = false
self.shadowNode.displayWithoutProcessing = true
self.shadowNode.image = UIImage(bundleImageName: "Components/Context Menu/Shadow")?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.isHidden = true
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground)
if displayTextSelectionTip {
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData)
@ -425,16 +465,26 @@ final class ContextActionsContainerNode: ASDisplayNode {
super.init()
self.addSubnode(self.shadowNode)
self.scrollNode.addSubnode(self.actionsNode)
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
self.addSubnode(self.scrollNode)
}
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
var widthClass = widthClass
if !self.blurBackground {
widthClass = .regular
}
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, transition: transition)
let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionsSize)
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
self.shadowNode.isHidden = widthClass == .compact
var contentSize = actionsSize
transition.updateFrame(node: self.actionsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionsSize))
transition.updateFrame(node: self.actionsNode, frame: bounds)
if let textSelectionTipNode = self.textSelectionTipNode {
contentSize.height += 8.0

View File

@ -73,8 +73,18 @@ public final class ContextMenuActionItem {
}
}
public protocol ContextMenuCustomNode: ASDisplayNode {
func updateLayout(constrainedWidth: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void)
func updateTheme(presentationData: PresentationData)
}
public protocol ContextMenuCustomItem {
func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode
}
public enum ContextMenuItem {
case action(ContextMenuActionItem)
case custom(ContextMenuCustomItem)
case separator
}

View File

@ -225,7 +225,7 @@ public func generateAdjustedStretchableFilledCircleImage(diameter: CGFloat, colo
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2) + 1, topCapHeight: Int(diameter / 2) + 1)
}
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {

View File

@ -121,11 +121,7 @@ private func logoutOptionsEntries(presentationData: PresentationData, canAddAcco
entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.LogoutOptions_ChangePhoneNumberTitle, presentationData.strings.LogoutOptions_ChangePhoneNumberText))
entries.append(.contactSupport(presentationData.theme, presentationData.strings.LogoutOptions_ContactSupportTitle, presentationData.strings.LogoutOptions_ContactSupportText))
entries.append(.logout(presentationData.theme, presentationData.strings.LogoutOptions_LogOut))
if hasWallets {
entries.append(.logoutInfo(presentationData.theme, presentationData.strings.LogoutOptions_LogOutWalletInfo))
} else {
entries.append(.logoutInfo(presentationData.theme, presentationData.strings.LogoutOptions_LogOutInfo))
}
return entries
}

View File

@ -873,6 +873,10 @@ public final class VoiceChatController: ViewController {
})
})))
}
items.append(.custom(VoiceChatVolumeContextItem(value: 1.0, valueChanged: { newValue in
strongSelf.call.setVolume(peerId: peer.id, volume: Double(newValue))
})))
}
guard !items.isEmpty else {

View File

@ -24,6 +24,7 @@ final class VoiceChatParticipantItem: ListViewItem {
case generic
case accent
case constructive
case destructive
}
case presence
@ -363,6 +364,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
wavesColor = textColorValue
case .constructive:
textColorValue = UIColor(rgb: 0x34c759)
case .destructive:
textColorValue = UIColor(rgb: 0xff3b30)
wavesColor = textColorValue
}
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
case .none:

View File

@ -0,0 +1,256 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
import ContextUI
import TelegramStringFormatting
final class VoiceChatRecordingContextItem: ContextMenuCustomItem {
fileprivate let timestamp: Double
fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void
init(timestamp: Double, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) {
self.timestamp = timestamp
self.action = action
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return VoiceChatRecordingContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
}
}
private let textFont = Font.regular(17.0)
private class IconNode: ASDisplayNode {
private let backgroundNode: ASImageNode
private let dotNode: ASImageNode
override init() {
let iconSize = 16.0 + (1.0 + UIScreenPixel) * 2.0
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateCircleImage(diameter: iconSize, lineWidth: 1.0 + UIScreenPixel, color: UIColor.white, backgroundColor: nil)
self.dotNode = ASImageNode()
self.dotNode.displaysAsynchronously = false
self.dotNode.displayWithoutProcessing = true
self.dotNode.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30))
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.dotNode)
}
override func didLoad() {
super.didLoad()
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
animation.duration = 0.5
animation.autoreverses = true
animation.repeatCount = Float.infinity
self.dotNode.layer.add(animation, forKey: "recording")
}
override func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
let dotSize = CGSize(width: 8.0, height: 8.0)
self.dotNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - dotSize.width) / 2.0, y: (self.bounds.height - dotSize.height) / 2.0), size: dotSize)
}
}
private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMenuCustomNode {
private let item: VoiceChatRecordingContextItem
private let presentationData: PresentationData
private let getController: () -> ContextController?
private let actionSelected: (ContextMenuActionResult) -> Void
private let backgroundNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let statusNode: ImmediateTextNode
private let iconNode: IconNode
private let buttonNode: HighlightTrackingButtonNode
private var timer: SwiftSignalKit.Timer?
private var pointerInteraction: PointerInteraction?
init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
let subtextFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isAccessibilityElement = false
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isAccessibilityElement = false
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
self.highlightedBackgroundNode.alpha = 0.0
self.textNode = ImmediateTextNode()
self.textNode.isAccessibilityElement = false
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_StopRecording, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
self.textNode.maximumNumberOfLines = 1
let statusNode = ImmediateTextNode()
statusNode.isAccessibilityElement = false
statusNode.isUserInteractionEnabled = false
statusNode.displaysAsynchronously = false
statusNode.attributedText = NSAttributedString(string: "0:00", font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
statusNode.maximumNumberOfLines = 1
self.statusNode = statusNode
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.isAccessibilityElement = true
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
self.iconNode = IconNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highligted in
guard let strongSelf = self else {
return
}
if highligted {
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.timer?.invalidate()
}
override func didLoad() {
super.didLoad()
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
if let strongSelf = self {
strongSelf.highlightedBackgroundNode.alpha = 0.75
}
}, willExit: { [weak self] in
if let strongSelf = self {
strongSelf.highlightedBackgroundNode.alpha = 0.0
}
})
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updateTime(transition: .immediate)
}, queue: Queue.mainQueue())
self.timer = timer
timer.start()
}
private var validLayout: CGSize?
func updateTime(transition: ContainedViewLayoutTransition) {
guard let size = self.validLayout else {
return
}
let currentTime = CFAbsoluteTimeGetCurrent()
let duration = currentTime - item.timestamp
let subtextFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
self.statusNode.attributedText = NSAttributedString(string: stringForDuration(Int32(duration)), font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
let sideInset: CGFloat = 16.0
let statusSize = self.statusNode.updateLayout(CGSize(width: size.width - sideInset - 32.0, height: .greatestFiniteMagnitude))
transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: self.statusNode.frame.minY), size: statusSize))
}
func updateLayout(constrainedWidth: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let sideInset: CGFloat = 16.0
let iconSideInset: CGFloat = 12.0
let verticalInset: CGFloat = 12.0
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
let standardIconWidth: CGFloat = 32.0
var rightTextInset: CGFloat = sideInset
if !iconSize.width.isZero {
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset
}
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
let statusSize = self.statusNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
let verticalSpacing: CGFloat = 2.0
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
self.validLayout = size
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: textSize))
if !iconSize.width.isZero {
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
})
}
func updateTheme(presentationData: PresentationData) {
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
let subtextFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
self.statusNode.attributedText = NSAttributedString(string: self.statusNode.attributedText?.string ?? "", font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
}
@objc private func buttonPressed() {
self.performAction()
}
func performAction() {
guard let controller = self.getController() else {
return
}
self.item.action(controller, { [weak self] result in
self?.actionSelected(result)
})
}
func setIsHighlighted(_ value: Bool) {
if value {
self.highlightedBackgroundNode.alpha = 1.0
} else {
self.highlightedBackgroundNode.alpha = 0.0
}
}
}

View File

@ -0,0 +1,382 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private final class VoiceChatSpeakerNodeDrawingState: NSObject {
let color: UIColor
let transition: CGFloat
let reverse: Bool
init(color: UIColor, transition: CGFloat, reverse: Bool) {
self.color = color
self.transition = transition
self.reverse = reverse
super.init()
}
}
private func generateWaveImage(color: UIColor, num: Int) -> UIImage? {
return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
context.translateBy(x: 6.0, y: 6.0)
switch num {
case 1:
let _ = try? drawSvgPath(context, path: "M15,9 C15.6666667,9.95023099 16,10.9487504 16,11.9955581 C16,13.0423659 15.6666667,14.0438465 15,15 S ")
case 2:
let _ = try? drawSvgPath(context, path: "M17.5,6.5 C18.8724771,8.24209014 19.5587156,10.072709 19.5587156,11.9918565 C19.5587156,13.9110041 18.8724771,15.7470519 17.5,17.5 S ")
case 3:
let _ = try? drawSvgPath(context, path: "M20,3.5 C22,6.19232113 23,9.02145934 23,11.9874146 C23,14.9533699 22,17.7908984 20,20.5 S ")
default:
break
}
})
}
final class VoiceChatSpeakerNode: ASDisplayNode {
class State: Equatable {
enum Value: Equatable {
case muted
case low
case medium
case high
}
let value: Value
let color: UIColor
init(value: Value, color: UIColor) {
self.value = value
self.color = color
}
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.value != rhs.value {
return false
}
if lhs.color.argb != rhs.color.argb {
return false
}
return true
}
}
private var hasState = false
private var state: State = State(value: .medium, color: .black)
private let iconNode: IconNode
private let waveNode1: ASImageNode
private let waveNode2: ASImageNode
private let waveNode3: ASImageNode
override init() {
self.iconNode = IconNode()
self.waveNode1 = ASImageNode()
self.waveNode1.displaysAsynchronously = false
self.waveNode1.displayWithoutProcessing = true
self.waveNode2 = ASImageNode()
self.waveNode2.displaysAsynchronously = false
self.waveNode2.displayWithoutProcessing = true
self.waveNode3 = ASImageNode()
self.waveNode3.displaysAsynchronously = false
self.waveNode3.displayWithoutProcessing = true
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.waveNode1)
self.addSubnode(self.waveNode2)
self.addSubnode(self.waveNode3)
}
private var animating = false
func update(state: State, animated: Bool, force: Bool = false) {
var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if self.state != state || force {
let previousState = self.state
self.state = state
if animated && self.animating {
return
}
if previousState.color != state.color {
self.waveNode1.image = generateWaveImage(color: state.color, num: 1)
self.waveNode2.image = generateWaveImage(color: state.color, num: 2)
self.waveNode3.image = generateWaveImage(color: state.color, num: 3)
}
self.update(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, completion: {
if self.state != state {
self.update(state: self.state, animated: animated, force: true)
}
})
}
}
private func update(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
self.animating = transition.isAnimated
self.iconNode.update(state: IconNode.State(muted: self.state.value == .muted, color: self.state.color), animated: transition.isAnimated)
let bounds = self.bounds
let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.iconNode.bounds = CGRect(origin: CGPoint(), size: bounds.size)
self.waveNode1.bounds = CGRect(origin: CGPoint(), size: bounds.size)
self.waveNode2.bounds = CGRect(origin: CGPoint(), size: bounds.size)
self.waveNode3.bounds = CGRect(origin: CGPoint(), size: bounds.size)
let iconPosition: CGPoint
let wave1Position: CGPoint
var wave1Alpha: CGFloat = 1.0
let wave2Position: CGPoint
var wave2Alpha: CGFloat = 1.0
let wave3Position: CGPoint
var wave3Alpha: CGFloat = 1.0
switch self.state.value {
case .muted:
iconPosition = CGPoint(x: center.x, y: center.y)
wave1Position = CGPoint(x: center.x + 4.0, y: center.y)
wave2Position = CGPoint(x: center.x + 4.0, y: center.y)
wave3Position = CGPoint(x: center.x + 4.0, y: center.y)
wave1Alpha = 0.0
wave2Alpha = 0.0
wave3Alpha = 0.0
case .low:
iconPosition = CGPoint(x: center.x - 1.0, y: center.y)
wave1Position = CGPoint(x: center.x + 3.0, y: center.y)
wave2Position = CGPoint(x: center.x + 3.0, y: center.y)
wave3Position = CGPoint(x: center.x + 3.0, y: center.y)
wave2Alpha = 0.0
wave3Alpha = 0.0
case .medium:
iconPosition = CGPoint(x: center.x - 3.0, y: center.y)
wave1Position = CGPoint(x: center.x + 1.0, y: center.y)
wave2Position = CGPoint(x: center.x + 1.0, y: center.y)
wave3Position = CGPoint(x: center.x + 1.0, y: center.y)
wave3Alpha = 0.0
case .high:
iconPosition = CGPoint(x: center.x - 4.0, y: center.y)
wave1Position = CGPoint(x: center.x, y: center.y)
wave2Position = CGPoint(x: center.x, y: center.y)
wave3Position = CGPoint(x: center.x, y: center.y)
}
transition.updatePosition(node: self.iconNode, position: iconPosition) { _ in
self.animating = false
completion()
}
transition.updatePosition(node: self.waveNode1, position: wave1Position)
transition.updatePosition(node: self.waveNode2, position: wave2Position)
transition.updatePosition(node: self.waveNode3, position: wave3Position)
transition.updateAlpha(node: self.waveNode1, alpha: wave1Alpha)
transition.updateAlpha(node: self.waveNode2, alpha: wave2Alpha)
transition.updateAlpha(node: self.waveNode3, alpha: wave3Alpha)
}
}
private class IconNode: ASDisplayNode {
class State: Equatable {
let muted: Bool
let color: UIColor
init(muted: Bool, color: UIColor) {
self.muted = muted
self.color = color
}
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.muted != rhs.muted {
return false
}
if lhs.color.argb != rhs.color.argb {
return false
}
return true
}
}
private class TransitionContext {
let startTime: Double
let duration: Double
let previousState: State
init(startTime: Double, duration: Double, previousState: State) {
self.startTime = startTime
self.duration = duration
self.previousState = previousState
}
}
private var animator: ConstantDisplayLinkAnimator?
private var hasState = false
private var state: State = State(muted: false, color: .black)
private var transitionContext: TransitionContext?
override init() {
super.init()
self.isOpaque = false
}
func update(state: State, animated: Bool) {
var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if self.state != state {
let previousState = self.state
self.state = state
if animated {
self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState)
}
self.updateAnimations()
self.setNeedsDisplay()
}
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
if let transitionContext = self.transitionContext {
if transitionContext.startTime + transitionContext.duration < timestamp {
self.transitionContext = nil
} else {
animate = 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()
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0
var color = self.state.color
var reverse = false
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 transitionContext.previousState.muted != self.state.muted {
transitionFraction = self.state.muted ? t : 1.0 - t
reverse = transitionContext.previousState.muted
}
if transitionContext.previousState.color.rgb != color.rgb {
color = transitionContext.previousState.color.interpolateTo(color, fraction: t)!
}
}
return VoiceChatSpeakerNodeDrawingState(color: color, transition: transitionFraction, reverse: reverse)
}
@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? VoiceChatSpeakerNodeDrawingState else {
return
}
let clearLineWidth: CGFloat = 4.0
let lineWidth: CGFloat = 1.0 + UIScreenPixel
context.setFillColor(parameters.color.cgColor)
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(lineWidth)
context.translateBy(x: 7.0, y: 6.0)
let _ = try? drawSvgPath(context, path: "M7,9 L10,9 L13.6080479,5.03114726 C13.9052535,4.70422117 14.4112121,4.6801279 14.7381382,4.97733344 C14.9049178,5.12895118 15,5.34388952 15,5.5692855 L15,18.4307145 C15,18.8725423 14.6418278,19.2307145 14.2,19.2307145 C13.974604,19.2307145 13.7596657,19.1356323 13.6080479,18.9688527 L10,15 L7,15 C6.44771525,15 6,14.5522847 6,14 L6,10 C6,9.44771525 6.44771525,9 7,9 S ")
context.translateBy(x: -7.0, y: -6.0)
if parameters.transition > 0.0 {
let startPoint: CGPoint
let endPoint: CGPoint
let origin: CGPoint
let length: CGFloat
if bounds.width > 30.0 {
origin = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel)
length = 17.0
} else {
origin = CGPoint(x: 5.0 + UIScreenPixel, y: 4.0 + UIScreenPixel)
length = 15.0
}
if parameters.reverse {
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
} else {
startPoint = origin
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
}
context.setBlendMode(.clear)
context.setLineWidth(clearLineWidth)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
context.setBlendMode(.normal)
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
}
}
}

View File

@ -0,0 +1,178 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import AppBundle
import ContextUI
final class VoiceChatVolumeContextItem: ContextMenuCustomItem {
private let value: CGFloat
private let valueChanged: (CGFloat) -> Void
init(value: CGFloat, valueChanged: @escaping (CGFloat) -> Void) {
self.value = value
self.valueChanged = valueChanged
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return VoiceChatVolumeContextItemNode(presentationData: presentationData, getController: getController, value: self.value, valueChanged: self.valueChanged)
}
}
private let textFont = Font.regular(17.0)
private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCustomNode {
private var presentationData: PresentationData
private let backgroundIconNode: VoiceChatSpeakerNode
private let backgroundTextNode: ImmediateTextNode
private let foregroundNode: ASDisplayNode
private let foregroundIconNode: VoiceChatSpeakerNode
private let foregroundTextNode: ImmediateTextNode
var value: CGFloat = 1.0 {
didSet {
self.updateValue()
}
}
private let valueChanged: (CGFloat) -> Void
private let hapticFeedback = HapticFeedback()
init(presentationData: PresentationData, getController: @escaping () -> ContextController?, value: CGFloat, valueChanged: @escaping (CGFloat) -> Void) {
self.presentationData = presentationData
self.value = value
self.valueChanged = valueChanged
self.backgroundIconNode = VoiceChatSpeakerNode()
self.backgroundTextNode = ImmediateTextNode()
self.backgroundTextNode.isAccessibilityElement = false
self.backgroundTextNode.isUserInteractionEnabled = false
self.backgroundTextNode.displaysAsynchronously = false
self.backgroundTextNode.textAlignment = .left
self.foregroundNode = ASDisplayNode()
self.foregroundNode.clipsToBounds = true
self.foregroundNode.isAccessibilityElement = false
self.foregroundNode.backgroundColor = UIColor(rgb: 0xffffff)
self.foregroundNode.isUserInteractionEnabled = false
self.foregroundIconNode = VoiceChatSpeakerNode()
self.foregroundTextNode = ImmediateTextNode()
self.foregroundTextNode.isAccessibilityElement = false
self.foregroundTextNode.isUserInteractionEnabled = false
self.foregroundTextNode.displaysAsynchronously = false
self.foregroundTextNode.textAlignment = .left
super.init()
self.isUserInteractionEnabled = true
self.addSubnode(self.backgroundIconNode)
self.addSubnode(self.backgroundTextNode)
self.addSubnode(self.foregroundNode)
self.foregroundNode.addSubnode(self.foregroundIconNode)
self.foregroundNode.addSubnode(self.foregroundTextNode)
}
override func didLoad() {
super.didLoad()
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
self.view.addGestureRecognizer(panGestureRecognizer)
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
self.updateValue()
}
private func updateValue(transition: ContainedViewLayoutTransition = .immediate) {
let width = self.frame.width
let value = self.value / 2.0
transition.updateFrameAdditive(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: value * width, height: self.frame.height)))
self.backgroundTextNode.attributedText = NSAttributedString(string: "\(Int(self.value * 100.0))%", font: textFont, textColor: UIColor(rgb: 0xffffff))
self.foregroundTextNode.attributedText = NSAttributedString(string: "\(Int(self.value * 100.0))%", font: textFont, textColor: UIColor(rgb: 0x000000))
let iconValue: VoiceChatSpeakerNode.State.Value
if value == 0.0 {
iconValue = .muted
} else if value < 0.33 {
iconValue = .low
} else if value < 0.66 {
iconValue = .medium
} else {
iconValue = .high
}
self.backgroundIconNode.update(state: VoiceChatSpeakerNode.State(value: iconValue, color: UIColor(rgb: 0xffffff)), animated: true)
self.foregroundIconNode.update(state: VoiceChatSpeakerNode.State(value: iconValue, color: UIColor(rgb: 0x000000)), animated: true)
let _ = self.backgroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
let _ = self.foregroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
}
func updateLayout(constrainedWidth: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let valueWidth: CGFloat = 70.0
let height: CGFloat = 45.0
var textSize = self.backgroundTextNode.updateLayout(CGSize(width: valueWidth, height: .greatestFiniteMagnitude))
textSize.width = valueWidth
return (CGSize(width: height * 3.0, height: height), { size, transition in
let leftInset: CGFloat = 17.0
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame)
transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame)
let iconSize = CGSize(width: 36.0, height: 36.0)
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSize.width - 10.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
transition.updateFrameAdditive(node: self.backgroundIconNode, frame: iconFrame)
transition.updateFrameAdditive(node: self.foregroundIconNode, frame: iconFrame)
self.updateValue(transition: transition)
})
}
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
break
case .changed:
let previousValue = self.value
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
let delta = translation / self.bounds.width * 2.0
self.value = max(0.0, min(2.0, self.value + delta))
gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view)
if self.value == 2.0 && previousValue != 2.0 {
self.backgroundIconNode.layer.animateScale(from: 1.0, to: 1.1, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.backgroundIconNode.layer.animateScale(from: 1.1, to: 1.0, duration: 0.16)
}
})
self.foregroundIconNode.layer.animateScale(from: 1.0, to: 1.1, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.foregroundIconNode.layer.animateScale(from: 1.1, to: 1.0, duration: 0.16)
}
})
self.hapticFeedback.impact(.soft)
} else if self.value == 0.0 && previousValue != 0.0 {
self.hapticFeedback.impact(.soft)
}
case .ended, .cancelled:
break
default:
break
}
}
}

View File

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

View File

@ -1,7 +1,7 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Shadow.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB