mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat volume UI
This commit is contained in:
parent
ec5b568d15
commit
a706c210de
@ -5783,3 +5783,21 @@ Sorry for the inconvenience.";
|
|||||||
"CallList.RecentCallsHeader" = "RECENT CALLS";
|
"CallList.RecentCallsHeader" = "RECENT CALLS";
|
||||||
|
|
||||||
"VoiceChat.PeerJoinedText" = "%@ joined the voice chat";
|
"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";
|
||||||
|
@ -36,12 +36,15 @@ private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecogn
|
|||||||
|
|
||||||
private enum ContextItemNode {
|
private enum ContextItemNode {
|
||||||
case action(ContextActionNode)
|
case action(ContextActionNode)
|
||||||
|
case custom(ContextMenuCustomNode)
|
||||||
case itemSeparator(ASDisplayNode)
|
case itemSeparator(ASDisplayNode)
|
||||||
case separator(ASDisplayNode)
|
case separator(ASDisplayNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class InnerActionsContainerNode: ASDisplayNode {
|
private final class InnerActionsContainerNode: ASDisplayNode {
|
||||||
|
private let blurBackground: Bool
|
||||||
private let presentationData: PresentationData
|
private let presentationData: PresentationData
|
||||||
|
private let containerNode: ASDisplayNode
|
||||||
private var effectView: UIVisualEffectView?
|
private var effectView: UIVisualEffectView?
|
||||||
private var itemNodes: [ContextItemNode]
|
private var itemNodes: [ContextItemNode]
|
||||||
private let feedbackTap: () -> Void
|
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) {
|
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.feedbackTap = feedbackTap
|
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] = []
|
var itemNodes: [ContextItemNode] = []
|
||||||
for i in 0 ..< items.count {
|
for i in 0 ..< items.count {
|
||||||
@ -80,6 +89,13 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||||
itemNodes.append(.itemSeparator(separatorNode))
|
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:
|
case .separator:
|
||||||
let separatorNode = ASDisplayNode()
|
let separatorNode = ASDisplayNode()
|
||||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
separatorNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||||
@ -91,23 +107,19 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.clipsToBounds = true
|
self.addSubnode(self.containerNode)
|
||||||
self.cornerRadius = 14.0
|
|
||||||
|
|
||||||
self.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
|
||||||
if !blurBackground {
|
|
||||||
self.backgroundColor = self.backgroundColor?.withAlphaComponent(1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.itemNodes.forEach({ itemNode in
|
self.itemNodes.forEach({ itemNode in
|
||||||
switch itemNode {
|
switch itemNode {
|
||||||
case let .action(actionNode):
|
case let .action(actionNode):
|
||||||
actionNode.isUserInteractionEnabled = false
|
actionNode.isUserInteractionEnabled = false
|
||||||
self.addSubnode(actionNode)
|
self.containerNode.addSubnode(actionNode)
|
||||||
|
case let .custom(itemNode):
|
||||||
|
self.containerNode.addSubnode(itemNode)
|
||||||
case let .itemSeparator(separatorNode):
|
case let .itemSeparator(separatorNode):
|
||||||
self.addSubnode(separatorNode)
|
self.containerNode.addSubnode(separatorNode)
|
||||||
case let .separator(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 {
|
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||||
var minActionsWidth: CGFloat = 250.0
|
var minActionsWidth: CGFloat = 250.0
|
||||||
|
|
||||||
switch widthClass {
|
switch widthClass {
|
||||||
case .compact:
|
case .compact:
|
||||||
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
||||||
@ -167,7 +180,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||||
}
|
}
|
||||||
self.effectView = effectView
|
self.effectView = effectView
|
||||||
self.view.insertSubview(effectView, at: 0)
|
self.containerNode.view.insertSubview(effectView, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
minActionsWidth = min(minActionsWidth, constrainedWidth)
|
minActionsWidth = min(minActionsWidth, constrainedWidth)
|
||||||
@ -199,6 +212,11 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
maxWidth = max(maxWidth, minSize.width)
|
maxWidth = max(maxWidth, minSize.width)
|
||||||
heightsAndCompletions.append((minSize.height, complete))
|
heightsAndCompletions.append((minSize.height, complete))
|
||||||
contentHeight += minSize.height
|
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:
|
case .itemSeparator:
|
||||||
heightsAndCompletions.append(nil)
|
heightsAndCompletions.append(nil)
|
||||||
contentHeight += UIScreenPixel
|
contentHeight += UIScreenPixel
|
||||||
@ -220,6 +238,13 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
itemCompletion(itemSize, transition)
|
itemCompletion(itemSize, transition)
|
||||||
verticalOffset += itemHeight
|
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):
|
case let .itemSeparator(separatorNode):
|
||||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
||||||
verticalOffset += UIScreenPixel
|
verticalOffset += UIScreenPixel
|
||||||
@ -230,8 +255,11 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let size = CGSize(width: maxWidth, height: verticalOffset)
|
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 {
|
if let effectView = self.effectView {
|
||||||
transition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(view: effectView, frame: bounds)
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
@ -241,6 +269,8 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
switch itemNode {
|
switch itemNode {
|
||||||
case let .action(action):
|
case let .action(action):
|
||||||
action.updateTheme(presentationData: presentationData)
|
action.updateTheme(presentationData: presentationData)
|
||||||
|
case let .custom(item):
|
||||||
|
item.updateTheme(presentationData: presentationData)
|
||||||
case let .separator(separator):
|
case let .separator(separator):
|
||||||
separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||||
case let .itemSeparator(itemSeparator):
|
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? {
|
func actionNode(at point: CGPoint) -> ContextActionNode? {
|
||||||
@ -393,6 +423,8 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ContextActionsContainerNode: ASDisplayNode {
|
final class ContextActionsContainerNode: ASDisplayNode {
|
||||||
|
private let blurBackground: Bool
|
||||||
|
private let shadowNode: ASImageNode
|
||||||
private let actionsNode: InnerActionsContainerNode
|
private let actionsNode: InnerActionsContainerNode
|
||||||
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||||
private let scrollNode: ASScrollNode
|
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) {
|
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)
|
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||||
if displayTextSelectionTip {
|
if displayTextSelectionTip {
|
||||||
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData)
|
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData)
|
||||||
@ -425,16 +465,26 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.shadowNode)
|
||||||
self.scrollNode.addSubnode(self.actionsNode)
|
self.scrollNode.addSubnode(self.actionsNode)
|
||||||
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
||||||
self.addSubnode(self.scrollNode)
|
self.addSubnode(self.scrollNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
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 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
|
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 {
|
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||||
contentSize.height += 8.0
|
contentSize.height += 8.0
|
||||||
|
@ -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 {
|
public enum ContextMenuItem {
|
||||||
case action(ContextMenuActionItem)
|
case action(ContextMenuActionItem)
|
||||||
|
case custom(ContextMenuCustomItem)
|
||||||
case separator
|
case separator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ public func generateAdjustedStretchableFilledCircleImage(diameter: CGFloat, colo
|
|||||||
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2) + 1, topCapHeight: Int(diameter / 2) + 1)
|
})?.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
|
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
if let backgroundColor = backgroundColor {
|
if let backgroundColor = backgroundColor {
|
||||||
|
@ -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(.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(.contactSupport(presentationData.theme, presentationData.strings.LogoutOptions_ContactSupportTitle, presentationData.strings.LogoutOptions_ContactSupportText))
|
||||||
entries.append(.logout(presentationData.theme, presentationData.strings.LogoutOptions_LogOut))
|
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))
|
entries.append(.logoutInfo(presentationData.theme, presentationData.strings.LogoutOptions_LogOutInfo))
|
||||||
}
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
guard !items.isEmpty else {
|
||||||
|
@ -24,6 +24,7 @@ final class VoiceChatParticipantItem: ListViewItem {
|
|||||||
case generic
|
case generic
|
||||||
case accent
|
case accent
|
||||||
case constructive
|
case constructive
|
||||||
|
case destructive
|
||||||
}
|
}
|
||||||
|
|
||||||
case presence
|
case presence
|
||||||
@ -363,6 +364,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||||||
wavesColor = textColorValue
|
wavesColor = textColorValue
|
||||||
case .constructive:
|
case .constructive:
|
||||||
textColorValue = UIColor(rgb: 0x34c759)
|
textColorValue = UIColor(rgb: 0x34c759)
|
||||||
|
case .destructive:
|
||||||
|
textColorValue = UIColor(rgb: 0xff3b30)
|
||||||
|
wavesColor = textColorValue
|
||||||
}
|
}
|
||||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
|
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
|
||||||
case .none:
|
case .none:
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
382
submodules/TelegramCallsUI/Sources/VoiceChatSpeakerNode.swift
Normal file
382
submodules/TelegramCallsUI/Sources/VoiceChatSpeakerNode.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "volsmall.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/volsmall.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/volsmall.pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
},
|
},
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"provides-namespace" : true
|
"provides-namespace" : true
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
21
submodules/TelegramUI/Images.xcassets/Components/Context Menu/Shadow.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Components/Context Menu/Shadow.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Components/Context Menu/Shadow.imageset/Shadow.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Components/Context Menu/Shadow.imageset/Shadow.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user