mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Merge commit 'ce883e2d24cc58f728e08c73bb77a1650e99b648'
This commit is contained in:
@@ -11,11 +11,13 @@ import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import ItemListPeerItem
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import AppBundle
|
||||
import RadialStatusNode
|
||||
import ContextUI
|
||||
import ShareController
|
||||
import DeleteChatPeerActionSheetItem
|
||||
import UndoUI
|
||||
|
||||
private final class VoiceChatControllerTitleView: UIView {
|
||||
private var theme: PresentationTheme
|
||||
@@ -80,41 +82,6 @@ private final class VoiceChatControllerTitleView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let foregroundNode: ASImageNode
|
||||
|
||||
private var validSize: CGSize?
|
||||
private var isOn: Bool?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.foregroundNode = ASImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.foregroundNode)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isOn: Bool) {
|
||||
if self.validSize != size {
|
||||
self.validSize = size
|
||||
|
||||
self.backgroundNode.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1C1C1E))
|
||||
}
|
||||
if self.isOn != isOn {
|
||||
self.isOn = isOn
|
||||
self.foregroundNode.image = UIImage(bundleImageName: isOn ? "Call/VoiceChatMicOn" : "Call/VoiceChatMicOff")
|
||||
}
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let image = self.foregroundNode.image {
|
||||
self.foregroundNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class VoiceChatController: ViewController {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
private struct ListTransition {
|
||||
@@ -124,17 +91,24 @@ public final class VoiceChatController: ViewController {
|
||||
let isLoading: Bool
|
||||
let isEmpty: Bool
|
||||
let crossFade: Bool
|
||||
let count: Int
|
||||
}
|
||||
|
||||
private final class Interaction {
|
||||
let updateIsMuted: (PeerId, Bool) -> Void
|
||||
let invitePeer: (Peer) -> Void
|
||||
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
||||
|
||||
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
|
||||
|
||||
let updateIsMuted: (PeerId, Bool) -> Void
|
||||
|
||||
init(
|
||||
updateIsMuted: @escaping (PeerId, Bool) -> Void
|
||||
updateIsMuted: @escaping (PeerId, Bool) -> Void,
|
||||
invitePeer: @escaping (Peer) -> Void,
|
||||
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
||||
) {
|
||||
self.updateIsMuted = updateIsMuted
|
||||
self.invitePeer = invitePeer
|
||||
self.peerContextAction = peerContextAction
|
||||
}
|
||||
|
||||
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
|
||||
@@ -147,7 +121,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func updateAudioLevels(levels: [(PeerId, Float)]) {
|
||||
func updateAudioLevels(_ levels: [(PeerId, Float)]) {
|
||||
for (peerId, level) in levels {
|
||||
if let pipe = self.audioLevels[peerId] {
|
||||
pipe.putNext(level)
|
||||
@@ -163,70 +137,85 @@ public final class VoiceChatController: ViewController {
|
||||
case speaking
|
||||
}
|
||||
|
||||
var participant: RenderedChannelParticipant
|
||||
var peer: Peer
|
||||
var presence: TelegramUserPresence?
|
||||
var activityTimestamp: Int32
|
||||
var state: State
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
|
||||
var stableId: PeerId {
|
||||
return self.participant.peer.id
|
||||
return self.peer.id
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
|
||||
if !lhs.peer.isEqual(rhs.peer) {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
return false
|
||||
}
|
||||
if lhs.activityTimestamp != rhs.activityTimestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.state != rhs.state {
|
||||
return false
|
||||
}
|
||||
if lhs.muteState != rhs.muteState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
|
||||
if lhs.activityTimestamp != rhs.activityTimestamp {
|
||||
return lhs.activityTimestamp > rhs.activityTimestamp
|
||||
}
|
||||
return lhs.participant.peer.id < rhs.participant.peer.id
|
||||
return lhs.peer.id < rhs.peer.id
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListViewItem {
|
||||
let peer = self.participant.peer
|
||||
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
|
||||
let peer = self.peer
|
||||
|
||||
let text: ItemListPeerItemText
|
||||
let text: VoiceChatParticipantItem.ParticipantText
|
||||
let icon: VoiceChatParticipantItem.Icon
|
||||
switch self.state {
|
||||
case .inactive:
|
||||
text = .presence
|
||||
icon = .invite
|
||||
case .listening:
|
||||
//TODO:localize
|
||||
let muteString: String
|
||||
if self.muteState != nil {
|
||||
muteString = " [muted]"
|
||||
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
|
||||
let microphoneColor: UIColor
|
||||
if let muteState = self.muteState {
|
||||
if muteState.canUnmute {
|
||||
microphoneColor = UIColor(rgb: 0x979797)
|
||||
} else {
|
||||
microphoneColor = UIColor(rgb: 0xff3b30)
|
||||
}
|
||||
} else {
|
||||
muteString = ""
|
||||
microphoneColor = UIColor(rgb: 0x979797)
|
||||
}
|
||||
text = .text("listening\(muteString)", .accent)
|
||||
icon = .microphone(self.muteState != nil, microphoneColor)
|
||||
case .speaking:
|
||||
//TODO:localize
|
||||
text = .text("speaking", .constructive)
|
||||
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
|
||||
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
||||
}
|
||||
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: context, peer: peer, height: .peerList, presence: self.participant.presences[self.participant.peer.id], text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
|
||||
//arguments.deleteIncludePeer(peer.peerId)
|
||||
})]), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
|
||||
switch self.state {
|
||||
case .inactive:
|
||||
break
|
||||
default:
|
||||
if self.participant.peer.id != context.account.peerId {
|
||||
interaction.updateIsMuted(self.participant.peer.id, self.muteState != nil ? false : true)
|
||||
}
|
||||
}
|
||||
}, setPeerIdWithRevealedOptions: { lhs, rhs in
|
||||
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
|
||||
}, removePeer: { id in
|
||||
//arguments.deleteIncludePeer(id)
|
||||
}, noInsets: true, audioLevel: peer.id == context.account.peerId ? nil : interaction.getAudioLevel(peer.id))
|
||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.presence, text: text, icon: icon, enabled: true, audioLevel: interaction.getAudioLevel(peer.id), action: {
|
||||
interaction.invitePeer(peer)
|
||||
}, contextAction: { node, gesture in
|
||||
interaction.peerContextAction(self, node, gesture)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListTransition {
|
||||
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade)
|
||||
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade, count: toEntries.count)
|
||||
}
|
||||
|
||||
private weak var controller: VoiceChatController?
|
||||
@@ -236,15 +225,15 @@ public final class VoiceChatController: ViewController {
|
||||
private var presentationData: PresentationData
|
||||
private var darkTheme: PresentationTheme
|
||||
|
||||
private let optionsButton: VoiceChatOptionsButton
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
private let audioOutputNode: CallControllerButtonItemNode
|
||||
private let leaveNode: CallControllerButtonItemNode
|
||||
private let actionButton: VoiceChatActionButton
|
||||
private let radialStatus: RadialStatusNode
|
||||
private let statusLabel: ImmediateTextNode
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
private var maxListHeight: CGFloat?
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
private var didSetContentsReady: Bool = false
|
||||
@@ -262,12 +251,15 @@ public final class VoiceChatController: ViewController {
|
||||
private var isMutedDisposable: Disposable?
|
||||
private var callStateDisposable: Disposable?
|
||||
|
||||
private var pushingToTalk = false
|
||||
|
||||
private var callState: PresentationGroupCallState?
|
||||
|
||||
private var audioOutputStateDisposable: Disposable?
|
||||
private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
|
||||
|
||||
private var audioLevelsDisposable: Disposable?
|
||||
private var myAudioLevelDisposable: Disposable?
|
||||
private var memberStatesDisposable: Disposable?
|
||||
|
||||
private var itemInteraction: Interaction?
|
||||
@@ -279,9 +271,12 @@ public final class VoiceChatController: ViewController {
|
||||
self.call = call
|
||||
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.darkTheme = defaultDarkPresentationTheme
|
||||
self.darkTheme = defaultDarkColorPresentationTheme
|
||||
|
||||
self.optionsButton = VoiceChatOptionsButton()
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
self.contentContainer.backgroundColor = .black
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.backgroundColor = self.darkTheme.list.itemBlocksBackgroundColor
|
||||
@@ -292,24 +287,117 @@ public final class VoiceChatController: ViewController {
|
||||
self.audioOutputNode = CallControllerButtonItemNode()
|
||||
self.leaveNode = CallControllerButtonItemNode()
|
||||
self.actionButton = VoiceChatActionButton()
|
||||
self.statusLabel = ImmediateTextNode()
|
||||
|
||||
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
|
||||
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||
})
|
||||
let invitePeer: (Peer) -> Void = { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.controller?.present(
|
||||
UndoOverlayController(
|
||||
presentationData: strongSelf.presentationData,
|
||||
content: .invitedToVoiceChat(
|
||||
context: strongSelf.context,
|
||||
peer: peer,
|
||||
text: strongSelf.presentationData.strings.VoiceChat_UserInvited(peer.compactDisplayTitle).0
|
||||
),
|
||||
elevatedLayout: false,
|
||||
action: { action in
|
||||
return true
|
||||
}
|
||||
),
|
||||
in: .current
|
||||
)
|
||||
}
|
||||
|
||||
self.backgroundColor = .black
|
||||
self.itemInteraction = Interaction(
|
||||
updateIsMuted: { [weak self] peerId, isMuted in
|
||||
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||
}, invitePeer: { peer in
|
||||
invitePeer(peer)
|
||||
}, peerContextAction: { [weak self] entry, sourceNode, gesture in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let peer = entry.peer
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
switch entry.state {
|
||||
case .inactive:
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_InvitePeer, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
invitePeer(peer)
|
||||
f(.default)
|
||||
})))
|
||||
default:
|
||||
if entry.muteState == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
|
||||
f(.default)
|
||||
})))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false)
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if peer.id != strongSelf.context.account.peerId {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme))
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: peer, chatPeer: peer, action: .removeFromGroup, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
|
||||
self.contentContainer.addSubnode(self.listNode)
|
||||
self.contentContainer.addSubnode(self.audioOutputNode)
|
||||
self.contentContainer.addSubnode(self.leaveNode)
|
||||
self.contentContainer.addSubnode(self.actionButton)
|
||||
self.contentContainer.addSubnode(self.statusLabel)
|
||||
self.contentContainer.addSubnode(self.radialStatus)
|
||||
|
||||
self.addSubnode(self.contentContainer)
|
||||
|
||||
@@ -406,7 +494,20 @@ public final class VoiceChatController: ViewController {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.itemInteraction?.updateAudioLevels(levels: levels)
|
||||
strongSelf.itemInteraction?.updateAudioLevels(levels)
|
||||
})
|
||||
|
||||
self.myAudioLevelDisposable = (call.myAudioLevel
|
||||
|> deliverOnMainQueue).start(next: { [weak self] level in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var effectiveLevel: Float = 0.0
|
||||
if let state = strongSelf.callState, !state.isMuted {
|
||||
effectiveLevel = level
|
||||
}
|
||||
strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)])
|
||||
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
|
||||
})
|
||||
|
||||
self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
|
||||
@@ -414,6 +515,48 @@ public final class VoiceChatController: ViewController {
|
||||
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.optionsButton.contextAction = { [weak self, weak optionsButton] sourceNode, gesture in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let strongOptionsButton = optionsButton else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { _ in return nil}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
})))
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
if let strongSelf = self {
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .url("url"), forcedTheme: strongSelf.darkTheme, forcedActionTitle: strongSelf.presentationData.strings.VoiceChat_CopyInviteLink)
|
||||
strongSelf.controller?.present(shareController, in: .window(.root))
|
||||
}
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
})))
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: strongOptionsButton.extractedContainerNode, keepInPlace: true)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
let optionsButtonItem = UIBarButtonItem(customDisplayNode: self.optionsButton)!
|
||||
optionsButtonItem.target = self
|
||||
optionsButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||||
self.controller?.navigationItem.setRightBarButton(optionsButtonItem, animated: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -425,6 +568,28 @@ public final class VoiceChatController: ViewController {
|
||||
self.audioOutputStateDisposable?.dispose()
|
||||
self.memberStatesDisposable?.dispose()
|
||||
self.audioLevelsDisposable?.dispose()
|
||||
self.myAudioLevelDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
|
||||
longTapRecognizer.minimumPressDuration = 0.3
|
||||
self.actionButton.view.addGestureRecognizer(longTapRecognizer)
|
||||
|
||||
let panRecognizer = CallPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
panRecognizer.shouldBegin = { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
@objc private func rightNavigationButtonAction() {
|
||||
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
|
||||
}
|
||||
|
||||
@objc private func leavePressed() {
|
||||
@@ -434,6 +599,21 @@ public final class VoiceChatController: ViewController {
|
||||
}))
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.pushingToTalk = true
|
||||
self.actionButton.pressing = true
|
||||
self.call.setIsMuted(false)
|
||||
case .ended, .cancelled:
|
||||
self.pushingToTalk = false
|
||||
self.actionButton.pressing = false
|
||||
self.call.setIsMuted(true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressed() {
|
||||
self.call.toggleIsMuted()
|
||||
}
|
||||
@@ -490,13 +670,6 @@ public final class VoiceChatController: ViewController {
|
||||
}))
|
||||
}
|
||||
|
||||
if hasMute {
|
||||
items.append(CallRouteActionSheetItem(title: self.presentationData.strings.Call_AudioRouteMute, icon: generateScaledImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false), selected: self.callState?.isMuted ?? true, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.call.toggleIsMuted()
|
||||
}))
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
@@ -513,11 +686,16 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let bottomAreaHeight: CGFloat = 302.0
|
||||
let bottomAreaHeight: CGFloat = 333.0
|
||||
|
||||
let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0)
|
||||
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y)))
|
||||
|
||||
var listHeight: CGFloat = 56.0
|
||||
if let maxListHeight = self.maxListHeight {
|
||||
listHeight = min(max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y), maxListHeight)
|
||||
}
|
||||
|
||||
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: listHeight))
|
||||
transition.updateFrame(node: self.listNode, frame: listFrame)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
@@ -526,9 +704,48 @@ public final class VoiceChatController: ViewController {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
let sideButtonSize = CGSize(width: 60.0, height: 60.0)
|
||||
let centralButtonSize = CGSize(width: 144.0, height: 144.0)
|
||||
let centralButtonSize = CGSize(width: 244.0, height: 244.0)
|
||||
let sideButtonInset: CGFloat = 27.0
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
|
||||
var isMicOn = false
|
||||
|
||||
let actionButtonState: VoiceChatActionButtonState
|
||||
let actionButtonTitle: String
|
||||
let actionButtonSubtitle: String
|
||||
let audioButtonAppearance: CallControllerButtonItemNode.Content.Appearance
|
||||
if let callState = callState {
|
||||
isMicOn = !callState.isMuted
|
||||
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
audioButtonAppearance = .color(.custom(0x1c1c1e))
|
||||
case .connected:
|
||||
actionButtonState = .active(state: isMicOn ? .on : .muted)
|
||||
if isMicOn {
|
||||
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
|
||||
actionButtonSubtitle = ""
|
||||
audioButtonAppearance = .color(.custom(0x005720))
|
||||
} else {
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
|
||||
actionButtonSubtitle = self.presentationData.strings.VoiceChat_UnmuteHelp
|
||||
audioButtonAppearance = .color(.custom(0x00274d))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionButtonState = .connecting
|
||||
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
|
||||
actionButtonSubtitle = ""
|
||||
audioButtonAppearance = .color(.custom(0x1c1c1e))
|
||||
}
|
||||
|
||||
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 144.0, height: 144.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, animated: true)
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
|
||||
var audioMode: CallControllerButtonsSpeakerMode = .none
|
||||
//var hasAudioRouteMenu: Bool = false
|
||||
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
|
||||
@@ -556,13 +773,13 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let soundImage: CallControllerButtonItemNode.Content.Image
|
||||
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = .color(.grayDimmed)
|
||||
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = audioButtonAppearance
|
||||
switch audioMode {
|
||||
case .none, .builtin:
|
||||
soundImage = .speaker
|
||||
case .speaker:
|
||||
soundImage = .speaker
|
||||
soundAppearance = .blurred(isFilled: true)
|
||||
// soundAppearance = .blurred(isFilled: false)
|
||||
case .headphones:
|
||||
soundImage = .bluetooth
|
||||
case let .bluetooth(type):
|
||||
@@ -576,46 +793,13 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: "audio", transition: .immediate)
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .animated(duration: 0.4, curve: .linear))
|
||||
|
||||
//TODO:localize
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: "leave", transition: .immediate)
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0x4d120e)), image: .end), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
|
||||
|
||||
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonInset - sideButtonSize.width, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
|
||||
var isMicOn = false
|
||||
if let callState = callState {
|
||||
isMicOn = !callState.isMuted
|
||||
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
self.radialStatus.isHidden = false
|
||||
|
||||
self.statusLabel.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(17.0), textColor: .white)
|
||||
case .connected:
|
||||
self.radialStatus.isHidden = true
|
||||
|
||||
if isMicOn {
|
||||
self.statusLabel.attributedText = NSAttributedString(string: "You're Live", font: Font.regular(17.0), textColor: .white)
|
||||
} else {
|
||||
self.statusLabel.attributedText = NSAttributedString(string: "Unmute", font: Font.regular(17.0), textColor: .white)
|
||||
}
|
||||
}
|
||||
|
||||
let statusSize = self.statusLabel.updateLayout(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude))
|
||||
self.statusLabel.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: actionButtonFrame.maxY + 12.0), size: statusSize)
|
||||
|
||||
self.radialStatus.transitionToState(.progress(color: UIColor(rgb: 0x00ACFF), lineWidth: 3.3, value: nil, cancelEnabled: false), animated: false)
|
||||
self.radialStatus.frame = actionButtonFrame.insetBy(dx: -3.3, dy: -3.3)
|
||||
}
|
||||
|
||||
self.actionButton.updateLayout(size: centralButtonSize, isOn: isMicOn)
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
|
||||
if isFirstTime {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
@@ -628,11 +812,17 @@ public final class VoiceChatController: ViewController {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
|
||||
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
|
||||
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
@@ -674,6 +864,19 @@ public final class VoiceChatController: ViewController {
|
||||
strongSelf.didSetContentsReady = true
|
||||
strongSelf.controller?.contentsReady.set(true)
|
||||
}
|
||||
|
||||
if !transition.deletions.isEmpty || !transition.insertions.isEmpty {
|
||||
var itemHeight: CGFloat = 56.0
|
||||
strongSelf.listNode.forEachVisibleItemNode { node in
|
||||
if node.frame.height > 0 {
|
||||
itemHeight = node.frame.height
|
||||
}
|
||||
}
|
||||
strongSelf.maxListHeight = CGFloat(transition.count) * itemHeight
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -706,6 +909,10 @@ public final class VoiceChatController: ViewController {
|
||||
var index: Int32 = 0
|
||||
|
||||
for member in members {
|
||||
if let user = member.peer as? TelegramUser, user.botInfo != nil || user.isDeleted {
|
||||
continue
|
||||
}
|
||||
|
||||
let memberState: PeerEntry.State
|
||||
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if member.peer.id == self.context.account.peerId {
|
||||
@@ -722,7 +929,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
entries.append(PeerEntry(
|
||||
participant: member,
|
||||
peer: member.peer,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState
|
||||
@@ -732,11 +939,59 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.currentEntries = entries
|
||||
|
||||
let presentationData = ItemListPresentationData(theme: self.darkTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings)
|
||||
|
||||
let presentationData = self.presentationData.withUpdated(theme: self.darkTheme)
|
||||
let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!)
|
||||
self.enqueueTransition(transition)
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: CallPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.contentContainer.clipsToBounds = true
|
||||
self.contentContainer.cornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
case .changed:
|
||||
let offset = recognizer.translation(in: self.view).y
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -offset
|
||||
|
||||
let transition = offset / bounds.height
|
||||
if transition > 0.02 {
|
||||
self.controller?.statusBar.statusBarStyle = .Ignore
|
||||
} else {
|
||||
self.controller?.statusBar.statusBarStyle = .White
|
||||
}
|
||||
self.bounds = bounds
|
||||
case .cancelled, .ended:
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 200.0 {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.contentContainer.cornerRadius = 0.0
|
||||
})
|
||||
self.controller?.statusBar.statusBarStyle = .White
|
||||
} else {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self?.controller?.dismissInteractively()
|
||||
var initialBounds = bounds
|
||||
initialBounds.origin = CGPoint()
|
||||
self?.bounds = initialBounds
|
||||
self?.controller?.statusBar.statusBarStyle = .White
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
@@ -762,13 +1017,13 @@ public final class VoiceChatController: ViewController {
|
||||
self.call = call
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: "Chat", target: self, action: #selector(self.closePressed))
|
||||
|
||||
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.VoiceChat_BackTitle, target: self, action: #selector(self.closePressed))
|
||||
self.navigationItem.leftBarButtonItem = backItem
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
@@ -814,6 +1069,16 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func dismissInteractively(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
self.didAppearOnce = false
|
||||
|
||||
completion?()
|
||||
self.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
@@ -832,3 +1097,25 @@ public final class VoiceChatController: ViewController {
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource {
|
||||
var keepInPlace: Bool
|
||||
let ignoreContentTouches: Bool = true
|
||||
|
||||
private let controller: ViewController
|
||||
private let sourceNode: ContextExtractedContentContainingNode
|
||||
|
||||
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.keepInPlace = keepInPlace
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user