mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat Improvements
This commit is contained in:
parent
d7694f997c
commit
845ca1e843
@ -5810,3 +5810,9 @@ Sorry for the inconvenience.";
|
||||
"Call.VoiceOver.VideoCallMissed" = "Missed Video Call";
|
||||
"Call.VoiceOver.VoiceCallCanceled" = "Cancelled Voice Call";
|
||||
"Call.VoiceOver.VideoCallCanceled" = "Cancelled Video Call";
|
||||
|
||||
"VoiceChat.UnmuteForMe" = "Unmute for Me";
|
||||
"VoiceChat.MuteForMe" = "Mute for Me";
|
||||
|
||||
"PeerInfo.ButtonVoiceChat" = "Voice Chat";
|
||||
"VoiceChat.OpenChat" = "Open Chat";
|
||||
|
@ -294,7 +294,7 @@ public protocol PresentationGroupCall: class {
|
||||
func toggleIsMuted()
|
||||
func setIsMuted(action: PresentationGroupCallMuteAction)
|
||||
func updateDefaultParticipantsAreMuted(isMuted: Bool)
|
||||
func setVolume(peerId: PeerId, volume: Double)
|
||||
func setVolume(peerId: PeerId, volume: Int32, sync: Bool)
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
|
||||
func updateMuteState(peerId: PeerId, isMuted: Bool)
|
||||
|
@ -89,7 +89,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
itemNodes.append(.itemSeparator(separatorNode))
|
||||
}
|
||||
case let .custom(item):
|
||||
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()
|
||||
@ -425,6 +425,7 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
||||
final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private let blurBackground: Bool
|
||||
private let shadowNode: ASImageNode
|
||||
private let additionalActionsNode: InnerActionsContainerNode?
|
||||
private let actionsNode: InnerActionsContainerNode
|
||||
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||
private let scrollNode: ASScrollNode
|
||||
@ -446,6 +447,14 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
self.shadowNode.isHidden = true
|
||||
|
||||
var items = items
|
||||
if let firstItem = items.first, case let .custom(item, additional) = firstItem, additional {
|
||||
self.additionalActionsNode = InnerActionsContainerNode(presentationData: presentationData, items: [firstItem], getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
items.removeFirst()
|
||||
} else {
|
||||
self.additionalActionsNode = nil
|
||||
}
|
||||
|
||||
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
if displayTextSelectionTip {
|
||||
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData)
|
||||
@ -466,6 +475,7 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.shadowNode)
|
||||
self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.scrollNode.addSubnode(self.actionsNode)
|
||||
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
@ -477,13 +487,24 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
widthClass = .regular
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, transition: transition)
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionsSize)
|
||||
|
||||
if let additionalActionsNode = self.additionalActionsNode {
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, transition: transition)
|
||||
contentSize = additionalActionsSize
|
||||
|
||||
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsSize))
|
||||
contentSize.height += 8.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize)
|
||||
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
self.shadowNode.isHidden = widthClass == .compact
|
||||
|
||||
var contentSize = actionsSize
|
||||
contentSize.width = max(contentSize.width, actionsSize.width)
|
||||
contentSize.height += actionsSize.height
|
||||
|
||||
transition.updateFrame(node: self.actionsNode, frame: bounds)
|
||||
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
|
@ -84,7 +84,7 @@ public protocol ContextMenuCustomItem {
|
||||
|
||||
public enum ContextMenuItem {
|
||||
case action(ContextMenuActionItem)
|
||||
case custom(ContextMenuCustomItem)
|
||||
case custom(ContextMenuCustomItem, Bool)
|
||||
case separator
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1511503333] = { return Api.InputEncryptedFile.parse_inputEncryptedFile($0) }
|
||||
dict[767652808] = { return Api.InputEncryptedFile.parse_inputEncryptedFileBigUploaded($0) }
|
||||
dict[-1456996667] = { return Api.messages.InactiveChats.parse_inactiveChats($0) }
|
||||
dict[1454409673] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
|
||||
dict[-1199443157] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
|
||||
dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) }
|
||||
dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) }
|
||||
dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) }
|
||||
|
@ -5438,27 +5438,29 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum GroupCallParticipant: TypeConstructorDescription {
|
||||
case groupCallParticipant(flags: Int32, userId: Int32, date: Int32, activeDate: Int32?, source: Int32)
|
||||
case groupCallParticipant(flags: Int32, userId: Int32, date: Int32, activeDate: Int32?, source: Int32, volume: Int32?, mutedCnt: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .groupCallParticipant(let flags, let userId, let date, let activeDate, let source):
|
||||
case .groupCallParticipant(let flags, let userId, let date, let activeDate, let source, let volume, let mutedCnt):
|
||||
if boxed {
|
||||
buffer.appendInt32(1454409673)
|
||||
buffer.appendInt32(-1199443157)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(userId, buffer: buffer, boxed: false)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(activeDate!, buffer: buffer, boxed: false)}
|
||||
serializeInt32(source, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 7) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 8) != 0 {serializeInt32(mutedCnt!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .groupCallParticipant(let flags, let userId, let date, let activeDate, let source):
|
||||
return ("groupCallParticipant", [("flags", flags), ("userId", userId), ("date", date), ("activeDate", activeDate), ("source", source)])
|
||||
case .groupCallParticipant(let flags, let userId, let date, let activeDate, let source, let volume, let mutedCnt):
|
||||
return ("groupCallParticipant", [("flags", flags), ("userId", userId), ("date", date), ("activeDate", activeDate), ("source", source), ("volume", volume), ("mutedCnt", mutedCnt)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -5473,13 +5475,19 @@ public extension Api {
|
||||
if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() }
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
var _6: Int32?
|
||||
if Int(_1!) & Int(1 << 7) != 0 {_6 = reader.readInt32() }
|
||||
var _7: Int32?
|
||||
if Int(_1!) & Int(1 << 8) != 0 {_7 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, userId: _2!, date: _3!, activeDate: _4, source: _5!)
|
||||
let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 8) == 0) || _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, userId: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, mutedCnt: _7)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -7287,22 +7287,6 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func editGroupCallMember(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1662282468)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
call.serialize(buffer, true)
|
||||
userId.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "phone.editGroupCallMember", parameters: [("flags", flags), ("call", call), ("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func inviteToGroupCall(call: Api.InputGroupCall, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(2067345760)
|
||||
@ -7406,6 +7390,23 @@ public extension Api {
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func editGroupCallMember(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser, volume: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1511559976)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
call.serialize(buffer, true)
|
||||
userId.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "phone.editGroupCallMember", parameters: [("flags", flags), ("call", call), ("userId", userId), ("volume", volume)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +325,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
return
|
||||
}
|
||||
|
||||
let panelData = currentGroupCall != nil ? nil : availableState
|
||||
let panelData = currentGroupCall != nil || availableState?.participantCount == 0 ? nil : availableState
|
||||
|
||||
let wasEmpty = strongSelf.groupCallPanelData == nil
|
||||
strongSelf.groupCallPanelData = panelData
|
||||
|
@ -171,7 +171,7 @@ private extension PresentationGroupCallState {
|
||||
networkState: .connecting,
|
||||
canManageCall: false,
|
||||
adminIds: Set(),
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true),
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||
defaultParticipantMuteState: nil
|
||||
)
|
||||
}
|
||||
@ -669,7 +669,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
ssrc: 0,
|
||||
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
||||
activityTimestamp: nil,
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true)
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||
volume: nil
|
||||
))
|
||||
participants.sort()
|
||||
}
|
||||
@ -1042,7 +1043,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
strongSelf.stateValue.muteState = muteState
|
||||
} else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute {
|
||||
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
|
||||
strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true)
|
||||
strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
|
||||
strongSelf.callContext?.setIsMuted(true)
|
||||
}
|
||||
}
|
||||
@ -1229,16 +1230,19 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.callContext?.setIsMuted(isEffectivelyMuted)
|
||||
|
||||
if isVisuallyMuted {
|
||||
self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true)
|
||||
self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
|
||||
} else {
|
||||
self.stateValue.muteState = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func setVolume(peerId: PeerId, volume: Double) {
|
||||
|
||||
public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) {
|
||||
for (ssrc, id) in self.ssrcMapping {
|
||||
if id == peerId {
|
||||
self.callContext?.setVolume(ssrc: ssrc, volume: volume)
|
||||
self.callContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0)
|
||||
if sync {
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: volume)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -1315,6 +1319,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
public func updateMuteState(peerId: PeerId, isMuted: Bool) {
|
||||
let canThenUnmute: Bool
|
||||
if isMuted {
|
||||
var mutedByYou = false
|
||||
if peerId == self.accountContext.account.peerId {
|
||||
canThenUnmute = true
|
||||
} else if self.stateValue.canManageCall {
|
||||
@ -1326,14 +1331,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
} else if self.stateValue.adminIds.contains(self.accountContext.account.peerId) {
|
||||
canThenUnmute = true
|
||||
} else {
|
||||
mutedByYou = true
|
||||
canThenUnmute = true
|
||||
}
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canThenUnmute) : nil)
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canThenUnmute, mutedByYou: mutedByYou) : nil, volume: nil)
|
||||
} else {
|
||||
if peerId == self.accountContext.account.peerId {
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil)
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil)
|
||||
} else if self.stateValue.canManageCall || self.stateValue.adminIds.contains(self.accountContext.account.peerId) {
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), volume: nil)
|
||||
} else {
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true))
|
||||
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -209,6 +209,7 @@ public final class VoiceChatController: ViewController {
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
var revealed: Bool?
|
||||
var canManageCall: Bool
|
||||
var volume: Int32?
|
||||
|
||||
var stableId: PeerId {
|
||||
return self.peer.id
|
||||
@ -236,6 +237,9 @@ public final class VoiceChatController: ViewController {
|
||||
if lhs.canManageCall != rhs.canManageCall {
|
||||
return false
|
||||
}
|
||||
if lhs.volume != rhs.volume {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -333,17 +337,30 @@ public final class VoiceChatController: ViewController {
|
||||
let icon: VoiceChatParticipantItem.Icon
|
||||
switch peerEntry.state {
|
||||
case .listening:
|
||||
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
|
||||
if let muteState = peerEntry.muteState, muteState.mutedByYou {
|
||||
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, .destructive)
|
||||
} else {
|
||||
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
|
||||
}
|
||||
let microphoneColor: UIColor
|
||||
if let muteState = peerEntry.muteState, !muteState.canUnmute {
|
||||
if let muteState = peerEntry.muteState, !muteState.canUnmute || muteState.mutedByYou {
|
||||
microphoneColor = UIColor(rgb: 0xff3b30)
|
||||
} else {
|
||||
microphoneColor = UIColor(rgb: 0x979797)
|
||||
}
|
||||
icon = .microphone(peerEntry.muteState != nil, microphoneColor)
|
||||
case .speaking:
|
||||
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
|
||||
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
||||
if let muteState = peerEntry.muteState, muteState.mutedByYou {
|
||||
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, .destructive)
|
||||
icon = .microphone(true, UIColor(rgb: 0xff3b30))
|
||||
} else {
|
||||
if let volume = peerEntry.volume, volume != 10000 {
|
||||
text = .text("\(volume / 100)% \(presentationData.strings.VoiceChat_StatusSpeaking)", .constructive)
|
||||
} else {
|
||||
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
|
||||
}
|
||||
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
||||
}
|
||||
case .invited:
|
||||
text = .text(presentationData.strings.VoiceChat_StatusInvited, .generic)
|
||||
icon = .invite(true)
|
||||
@ -355,7 +372,7 @@ public final class VoiceChatController: ViewController {
|
||||
interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId)
|
||||
}, action: {
|
||||
interaction.openPeer(peer.id)
|
||||
}, contextAction: peer.id == context.account.peerId || !peerEntry.canManageCall ? nil : { node, gesture in
|
||||
}, contextAction: peer.id == context.account.peerId ? nil : { node, gesture in
|
||||
interaction.peerContextAction(peerEntry, node, gesture)
|
||||
})
|
||||
}
|
||||
@ -387,6 +404,7 @@ public final class VoiceChatController: ViewController {
|
||||
private let topPanelNode: ASDisplayNode
|
||||
private let topPanelEdgeNode: ASDisplayNode
|
||||
private let topPanelBackgroundNode: ASDisplayNode
|
||||
private let recButton: VoiceChatHeaderButton
|
||||
private let optionsButton: VoiceChatHeaderButton
|
||||
private let closeButton: VoiceChatHeaderButton
|
||||
private let topCornersNode: ASImageNode
|
||||
@ -493,6 +511,9 @@ public final class VoiceChatController: ViewController {
|
||||
self.topPanelEdgeNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
}
|
||||
|
||||
self.recButton = VoiceChatHeaderButton(rec: true)
|
||||
self.recButton.setImage(optionsBackgroundImage(dark: false))
|
||||
self.recButton.isHidden = true
|
||||
self.optionsButton = VoiceChatHeaderButton()
|
||||
self.optionsButton.setImage(optionsButtonImage(dark: false))
|
||||
self.closeButton = VoiceChatHeaderButton()
|
||||
@ -792,6 +813,16 @@ public final class VoiceChatController: ViewController {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if peer.id != strongSelf.context.account.peerId {
|
||||
if let muteState = entry.muteState, muteState.mutedByYou {
|
||||
} else {
|
||||
items.append(.custom(VoiceChatVolumeContextItem(value: entry.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { newValue, finished in
|
||||
if finished && newValue.isZero {
|
||||
|
||||
}
|
||||
strongSelf.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished)
|
||||
}), true))
|
||||
}
|
||||
|
||||
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) {
|
||||
if callState.adminIds.contains(peer.id) {
|
||||
if let _ = entry.muteState {
|
||||
@ -832,7 +863,42 @@ public final class VoiceChatController: ViewController {
|
||||
})))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let muteState = entry.muteState, muteState.mutedByYou {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmuteForMe, 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)
|
||||
})))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MuteForMe, 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)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_OpenChat, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.itemInteraction?.openPeer(peer.id)
|
||||
f(.default)
|
||||
})))
|
||||
|
||||
if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id)) {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
|
||||
@ -873,10 +939,6 @@ 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 {
|
||||
@ -896,6 +958,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.topPanelNode.addSubnode(self.topPanelEdgeNode)
|
||||
self.topPanelNode.addSubnode(self.topPanelBackgroundNode)
|
||||
self.topPanelNode.addSubnode(self.titleNode)
|
||||
self.topPanelNode.addSubnode(self.recButton)
|
||||
self.topPanelNode.addSubnode(self.optionsButton)
|
||||
self.topPanelNode.addSubnode(self.closeButton)
|
||||
self.topPanelNode.addSubnode(self.topCornersNode)
|
||||
@ -1080,7 +1143,7 @@ public final class VoiceChatController: ViewController {
|
||||
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 {
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1123,6 +1186,15 @@ public final class VoiceChatController: ViewController {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: CFAbsoluteTimeGetCurrent(), action: { (_, f) in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
}), false))
|
||||
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if let callState = strongSelf.callState, callState.canManageCall {
|
||||
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)
|
||||
@ -1152,12 +1224,22 @@ public final class VoiceChatController: ViewController {
|
||||
strongSelf.controller?.present(alert, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
|
||||
let optionsButton: VoiceChatHeaderButton
|
||||
if !strongSelf.recButton.isHidden {
|
||||
optionsButton = strongSelf.recButton
|
||||
} else {
|
||||
optionsButton = strongSelf.optionsButton
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: strongOptionsButton.extractedContainerNode, keepInPlace: false, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: optionsButton.extractedContainerNode, keepInPlace: false, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
self.recButton.contextAction = self.optionsButton.contextAction
|
||||
|
||||
self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
|
||||
self.recButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.actionButtonColorDisposable = (self.actionButton.outerColor
|
||||
@ -1566,6 +1648,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: isFullscreen)
|
||||
|
||||
self.recButton.setImage(optionsBackgroundImage(dark: isFullscreen), animated: transition.isAnimated)
|
||||
self.optionsButton.setImage(optionsButtonImage(dark: isFullscreen), animated: transition.isAnimated)
|
||||
self.closeButton.setImage(closeButtonImage(dark: isFullscreen), animated: transition.isAnimated)
|
||||
|
||||
@ -1670,6 +1753,7 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.updateTitle(transition: transition)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: CGSize(width: size.width, height: 44.0)))
|
||||
transition.updateFrame(node: self.recButton, frame: CGRect(origin: CGPoint(x: 20.0, y: 18.0), size: CGSize(width: 58.0, height: 28.0)))
|
||||
transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
|
||||
@ -2019,7 +2103,8 @@ public final class VoiceChatController: ViewController {
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
canManageCall: callState?.canManageCall ?? false
|
||||
canManageCall: self.callState?.canManageCall ?? false,
|
||||
volume: member.volume
|
||||
)))
|
||||
index += 1
|
||||
}
|
||||
@ -2030,8 +2115,9 @@ public final class VoiceChatController: ViewController {
|
||||
presence: nil,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: .listening,
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true),
|
||||
canManageCall: callState?.canManageCall ?? false
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||
canManageCall: self.callState?.canManageCall ?? false,
|
||||
volume: nil
|
||||
)), at: 1)
|
||||
}
|
||||
|
||||
@ -2047,7 +2133,8 @@ public final class VoiceChatController: ViewController {
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: .invited,
|
||||
muteState: nil,
|
||||
canManageCall: false
|
||||
canManageCall: false,
|
||||
volume: nil
|
||||
)))
|
||||
index += 1
|
||||
}
|
||||
|
@ -3,6 +3,15 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
func optionsBackgroundImage(dark: Bool) -> UIImage? {
|
||||
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14)
|
||||
}
|
||||
|
||||
func optionsButtonImage(dark: Bool) -> UIImage? {
|
||||
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
@ -45,13 +54,30 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
|
||||
|
||||
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
init() {
|
||||
var textNode: ImmediateTextNode?
|
||||
var dotNode: ASImageNode?
|
||||
|
||||
init(rec: Bool = false) {
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.isGestureEnabled = false
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.contentMode = .scaleToFill
|
||||
|
||||
if rec {
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode?.attributedText = NSAttributedString(string: "REC", font: Font.regular(12.0), textColor: .white)
|
||||
if let textNode = self.textNode {
|
||||
let textSize = textNode.updateLayout(CGSize(width: 58.0, height: 28.0))
|
||||
textNode.frame = CGRect(origin: CGPoint(), size: textSize)
|
||||
}
|
||||
self.dotNode = ASImageNode()
|
||||
self.dotNode?.displaysAsynchronously = false
|
||||
self.dotNode?.displayWithoutProcessing = true
|
||||
self.dotNode?.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30))
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
@ -60,6 +86,11 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
if rec, let textNode = self.textNode, let dotNode = self.dotNode {
|
||||
self.extractedContainerNode.contentNode.addSubnode(textNode)
|
||||
self.extractedContainerNode.contentNode.addSubnode(dotNode)
|
||||
}
|
||||
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
guard let strongSelf = self, let _ = strongSelf.contextAction else {
|
||||
return false
|
||||
@ -75,7 +106,7 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
|
||||
|
||||
self.iconNode.image = optionsButtonImage(dark: false)
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: rec ? 58.0 : 28.0, height: 28.0))
|
||||
self.extractedContainerNode.frame = self.containerNode.bounds
|
||||
self.extractedContainerNode.contentRect = self.containerNode.bounds
|
||||
self.iconNode.frame = self.containerNode.bounds
|
||||
@ -96,10 +127,29 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
self.view.isOpaque = false
|
||||
|
||||
if let dotNode = self.dotNode {
|
||||
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
|
||||
dotNode.layer.add(animation, forKey: "recording")
|
||||
}
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: 28.0, height: 28.0)
|
||||
return CGSize(width: self.dotNode != nil ? 58.0 : 28.0, height: 28.0)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
if let dotNode = self.dotNode, let textNode = self.textNode {
|
||||
dotNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 8.0, height: 8.0))
|
||||
textNode.frame = CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: textNode.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
func onLayout() {
|
||||
|
@ -8,9 +8,9 @@ import ContextUI
|
||||
|
||||
final class VoiceChatVolumeContextItem: ContextMenuCustomItem {
|
||||
private let value: CGFloat
|
||||
private let valueChanged: (CGFloat) -> Void
|
||||
private let valueChanged: (CGFloat, Bool) -> Void
|
||||
|
||||
init(value: CGFloat, valueChanged: @escaping (CGFloat) -> Void) {
|
||||
init(value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
|
||||
self.value = value
|
||||
self.valueChanged = valueChanged
|
||||
}
|
||||
@ -38,11 +38,11 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu
|
||||
}
|
||||
}
|
||||
|
||||
private let valueChanged: (CGFloat) -> Void
|
||||
private let valueChanged: (CGFloat, Bool) -> Void
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(presentationData: PresentationData, getController: @escaping () -> ContextController?, value: CGFloat, valueChanged: @escaping (CGFloat) -> Void) {
|
||||
init(presentationData: PresentationData, getController: @escaping () -> ContextController?, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.value = value
|
||||
self.valueChanged = valueChanged
|
||||
@ -169,7 +169,12 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu
|
||||
} else if self.value == 0.0 && previousValue != 0.0 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.valueChanged(self.value, false)
|
||||
case .ended:
|
||||
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))
|
||||
self.valueChanged(self.value, true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
|
@ -88,23 +88,28 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
|
||||
|
||||
loop: for participant in participants {
|
||||
switch participant {
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source):
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source, volume, mutedCnt):
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
|
||||
let ssrc = UInt32(bitPattern: source)
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
continue loop
|
||||
}
|
||||
let muted = (flags & (1 << 0)) != 0
|
||||
let mutedByYou = (flags & (1 << 9)) != 0
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
if muted {
|
||||
let canUnmute = (flags & (1 << 2)) != 0
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute)
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou)
|
||||
} else if mutedByYou {
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou)
|
||||
}
|
||||
parsedParticipants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: peer,
|
||||
ssrc: ssrc,
|
||||
joinTimestamp: date,
|
||||
activityTimestamp: activeDate.flatMap(Double.init),
|
||||
muteState: muteState
|
||||
muteState: muteState,
|
||||
volume: volume
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -223,23 +228,28 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash
|
||||
|
||||
loop: for participant in participants {
|
||||
switch participant {
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source):
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source, volume, mutedCnt):
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
|
||||
let ssrc = UInt32(bitPattern: source)
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
continue loop
|
||||
}
|
||||
let muted = (flags & (1 << 0)) != 0
|
||||
let mutedByYou = (flags & (1 << 9)) != 0
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
if muted {
|
||||
let canUnmute = (flags & (1 << 2)) != 0
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute)
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou)
|
||||
} else if mutedByYou {
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou)
|
||||
}
|
||||
parsedParticipants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: peer,
|
||||
ssrc: ssrc,
|
||||
joinTimestamp: date,
|
||||
activityTimestamp: activeDate.flatMap(Double.init),
|
||||
muteState: muteState
|
||||
muteState: muteState,
|
||||
volume: volume
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -544,9 +554,11 @@ public final class GroupCallParticipantsContext {
|
||||
public struct Participant: Equatable, Comparable {
|
||||
public struct MuteState: Equatable {
|
||||
public var canUnmute: Bool
|
||||
public var mutedByYou: Bool
|
||||
|
||||
public init(canUnmute: Bool) {
|
||||
public init(canUnmute: Bool, mutedByYou: Bool) {
|
||||
self.canUnmute = canUnmute
|
||||
self.mutedByYou = mutedByYou
|
||||
}
|
||||
}
|
||||
|
||||
@ -555,19 +567,22 @@ public final class GroupCallParticipantsContext {
|
||||
public var joinTimestamp: Int32
|
||||
public var activityTimestamp: Double?
|
||||
public var muteState: MuteState?
|
||||
public var volume: Int32?
|
||||
|
||||
public init(
|
||||
peer: Peer,
|
||||
ssrc: UInt32,
|
||||
joinTimestamp: Int32,
|
||||
activityTimestamp: Double?,
|
||||
muteState: MuteState?
|
||||
muteState: MuteState?,
|
||||
volume: Int32?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.ssrc = ssrc
|
||||
self.joinTimestamp = joinTimestamp
|
||||
self.activityTimestamp = activityTimestamp
|
||||
self.muteState = muteState
|
||||
self.volume = volume
|
||||
}
|
||||
|
||||
public static func ==(lhs: Participant, rhs: Participant) -> Bool {
|
||||
@ -586,6 +601,9 @@ public final class GroupCallParticipantsContext {
|
||||
if lhs.muteState != rhs.muteState {
|
||||
return false
|
||||
}
|
||||
if lhs.volume != rhs.volume {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -626,12 +644,16 @@ public final class GroupCallParticipantsContext {
|
||||
private struct OverlayState: Equatable {
|
||||
struct MuteStateChange: Equatable {
|
||||
var state: Participant.MuteState?
|
||||
var volume: Int32?
|
||||
var disposable: Disposable
|
||||
|
||||
static func ==(lhs: MuteStateChange, rhs: MuteStateChange) -> Bool {
|
||||
if lhs.state != rhs.state {
|
||||
return false
|
||||
}
|
||||
if lhs.volume != rhs.volume {
|
||||
return false
|
||||
}
|
||||
if lhs.disposable !== rhs.disposable {
|
||||
return false
|
||||
}
|
||||
@ -669,6 +691,7 @@ public final class GroupCallParticipantsContext {
|
||||
public var activityTimestamp: Double?
|
||||
public var muteState: Participant.MuteState?
|
||||
public var participationStatusChange: ParticipationStatusChange
|
||||
public var volume: Int32?
|
||||
}
|
||||
|
||||
public var participantUpdates: [ParticipantUpdate]
|
||||
@ -716,6 +739,7 @@ public final class GroupCallParticipantsContext {
|
||||
for i in 0 ..< publicState.participants.count {
|
||||
if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] {
|
||||
publicState.participants[i].muteState = pendingMuteState.state
|
||||
publicState.participants[i].volume = pendingMuteState.volume
|
||||
}
|
||||
}
|
||||
return publicState
|
||||
@ -1009,7 +1033,8 @@ public final class GroupCallParticipantsContext {
|
||||
ssrc: participantUpdate.ssrc,
|
||||
joinTimestamp: participantUpdate.joinTimestamp,
|
||||
activityTimestamp: activityTimestamp,
|
||||
muteState: participantUpdate.muteState
|
||||
muteState: participantUpdate.muteState,
|
||||
volume: participantUpdate.volume
|
||||
)
|
||||
updatedParticipants.append(participant)
|
||||
}
|
||||
@ -1067,7 +1092,7 @@ public final class GroupCallParticipantsContext {
|
||||
}))
|
||||
}
|
||||
|
||||
public func updateMuteState(peerId: PeerId, muteState: Participant.MuteState?) {
|
||||
public func updateMuteState(peerId: PeerId, muteState: Participant.MuteState?, volume: Int32?) {
|
||||
if let current = self.stateValue.overlayState.pendingMuteStateChanges[peerId] {
|
||||
if current.state == muteState {
|
||||
return
|
||||
@ -1078,7 +1103,7 @@ public final class GroupCallParticipantsContext {
|
||||
|
||||
for participant in self.stateValue.state.participants {
|
||||
if participant.peer.id == peerId {
|
||||
if participant.muteState == muteState {
|
||||
if participant.muteState == muteState && participant.volume == volume {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1087,6 +1112,7 @@ public final class GroupCallParticipantsContext {
|
||||
let disposable = MetaDisposable()
|
||||
self.stateValue.overlayState.pendingMuteStateChanges[peerId] = OverlayState.MuteStateChange(
|
||||
state: muteState,
|
||||
volume: volume,
|
||||
disposable: disposable
|
||||
)
|
||||
|
||||
@ -1102,11 +1128,13 @@ public final class GroupCallParticipantsContext {
|
||||
return .single(nil)
|
||||
}
|
||||
var flags: Int32 = 0
|
||||
if let muteState = muteState, (!muteState.canUnmute || peerId == account.peerId) {
|
||||
if let muteState = muteState, (!muteState.canUnmute || peerId == account.peerId || muteState.mutedByYou) {
|
||||
flags |= 1 << 0
|
||||
} else if let _ = volume {
|
||||
flags |= 1 << 1
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.phone.editGroupCallMember(flags: flags, call: .inputGroupCall(id: id, accessHash: accessHash), userId: inputUser))
|
||||
return account.network.request(Api.functions.phone.editGroupCallMember(flags: flags, call: .inputGroupCall(id: id, accessHash: accessHash), userId: inputUser, volume: volume))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
@ -1152,7 +1180,6 @@ public final class GroupCallParticipantsContext {
|
||||
}
|
||||
self.stateValue.state.defaultParticipantsAreMuted.isMuted = isMuted
|
||||
|
||||
|
||||
self.updateDefaultMuteDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 0, call: .inputGroupCall(id: self.id, accessHash: self.accessHash), joinMuted: isMuted ? .boolTrue : .boolFalse))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] updates in
|
||||
guard let strongSelf = self else {
|
||||
@ -1166,13 +1193,17 @@ public final class GroupCallParticipantsContext {
|
||||
extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate {
|
||||
init(_ apiParticipant: Api.GroupCallParticipant) {
|
||||
switch apiParticipant {
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source):
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source, volume, mutedCnt):
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
|
||||
let ssrc = UInt32(bitPattern: source)
|
||||
let muted = (flags & (1 << 0)) != 0
|
||||
let mutedByYou = (flags & (1 << 9)) != 0
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
if muted {
|
||||
let canUnmute = (flags & (1 << 2)) != 0
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute)
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou)
|
||||
} else if mutedByYou {
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou)
|
||||
}
|
||||
let isRemoved = (flags & (1 << 1)) != 0
|
||||
let justJoined = (flags & (1 << 4)) != 0
|
||||
@ -1192,7 +1223,8 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate {
|
||||
joinTimestamp: date,
|
||||
activityTimestamp: activeDate.flatMap(Double.init),
|
||||
muteState: muteState,
|
||||
participationStatusChange: participationStatusChange
|
||||
participationStatusChange: participationStatusChange,
|
||||
volume: volume
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1203,13 +1235,17 @@ extension GroupCallParticipantsContext.Update.StateUpdate {
|
||||
var participantUpdates: [GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate] = []
|
||||
for participant in participants {
|
||||
switch participant {
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source):
|
||||
case let .groupCallParticipant(flags, userId, date, activeDate, source, volume, mutedCnt):
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
|
||||
let ssrc = UInt32(bitPattern: source)
|
||||
let muted = (flags & (1 << 0)) != 0
|
||||
let mutedByYou = (flags & (1 << 9)) != 0
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
if muted {
|
||||
let canUnmute = (flags & (1 << 2)) != 0
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute)
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou)
|
||||
} else if mutedByYou {
|
||||
muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou)
|
||||
}
|
||||
let isRemoved = (flags & (1 << 1)) != 0
|
||||
let justJoined = (flags & (1 << 4)) != 0
|
||||
@ -1229,7 +1265,8 @@ extension GroupCallParticipantsContext.Update.StateUpdate {
|
||||
joinTimestamp: date,
|
||||
activityTimestamp: activeDate.flatMap(Double.init),
|
||||
muteState: muteState,
|
||||
participationStatusChange: participationStatusChange
|
||||
participationStatusChange: participationStatusChange,
|
||||
volume: volume
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -25,26 +25,6 @@ public final class FoundStickerItem: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension MutableCollection {
|
||||
mutating func shuffle() {
|
||||
let c = count
|
||||
guard c > 1 else { return }
|
||||
for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
|
||||
let d: IndexDistance = numericCast(arc4random_uniform(numericCast(unshuffledCount)))
|
||||
let i = index(firstUnshuffled, offsetBy: d)
|
||||
swapAt(firstUnshuffled, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence {
|
||||
func shuffled() -> [Element] {
|
||||
var result = Array(self)
|
||||
result.shuffle()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200)
|
||||
|
||||
public struct SearchStickersScope: OptionSet {
|
||||
@ -97,8 +77,8 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
|
||||
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers) {
|
||||
if let item = entry.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile {
|
||||
if !currentItems.contains(file.fileId) {
|
||||
for case let .Sticker(sticker) in file.attributes {
|
||||
if sticker.displayText.hasPrefix(query) {
|
||||
for case let .Sticker(displayText, _, _) in file.attributes {
|
||||
if displayText.hasPrefix(query) {
|
||||
matchingRecentItemsIds.insert(file.fileId)
|
||||
}
|
||||
recentItemsIds.insert(file.fileId)
|
||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 122
|
||||
return 123
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVoiceChat.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVoiceChat.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profile_voice.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVoiceChat.imageset/profile_voice.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVoiceChat.imageset/profile_voice.pdf
vendored
Normal file
Binary file not shown.
@ -1,9 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -504,7 +504,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
break
|
||||
}
|
||||
}
|
||||
case let .groupPhoneCall(callId, accessHash, _), let .inviteToGroupPhoneCall(callId, accessHash, _):
|
||||
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
||||
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
||||
strongSelf.context.joinGroupCall(peerId: message.id.peerId, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash))
|
||||
} else {
|
||||
@ -556,7 +556,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||||
}
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
}, completed: {
|
||||
dismissStatus?()
|
||||
}))
|
||||
}
|
||||
@ -708,7 +708,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
|
||||
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: message.text, snapshots: snapshots, transitionCompletion: {
|
||||
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: "", snapshots: snapshots, transitionCompletion: {
|
||||
transitionCompletion()
|
||||
}, presentStickers: { [weak self] completion in
|
||||
if let strongSelf = self {
|
||||
@ -854,9 +854,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}, navigateToMessage: { [weak self] fromId, id in
|
||||
self?.navigateToMessage(from: fromId, to: .id(id), forceInCurrentChat: fromId.peerId == id.peerId)
|
||||
}, navigateToMessageStandalone: { [weak self] id in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
self?.navigateToMessage(from: nil, to: .id(id), forceInCurrentChat: false)
|
||||
}, tapMessage: nil, clickThroughMessage: { [weak self] in
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
|
@ -65,6 +65,26 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
|
||||
return updates
|
||||
}
|
||||
|
||||
struct StickersSearchConfiguration {
|
||||
static var defaultValue: StickersSearchConfiguration {
|
||||
return StickersSearchConfiguration(disableLocalSuggestions: false)
|
||||
}
|
||||
|
||||
public let disableLocalSuggestions: Bool
|
||||
|
||||
fileprivate init(disableLocalSuggestions: Bool) {
|
||||
self.disableLocalSuggestions = disableLocalSuggestions
|
||||
}
|
||||
|
||||
static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration {
|
||||
if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool {
|
||||
return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi)
|
||||
} else {
|
||||
return .defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||
switch inputQuery {
|
||||
case let .emoji(query):
|
||||
@ -79,18 +99,30 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
} else {
|
||||
signal = .single({ _ in return .stickers([]) })
|
||||
}
|
||||
let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in
|
||||
|
||||
let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||||
|> map { preferencesView -> StickersSearchConfiguration in
|
||||
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||
return StickersSearchConfiguration.with(appConfiguration: appConfiguration)
|
||||
}
|
||||
let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in
|
||||
let stickerSettings: StickerSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings
|
||||
return stickerSettings
|
||||
}
|
||||
|
||||
let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = combineLatest(stickerConfiguration, stickerSettings)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||
|> mapToSignal { stickerConfiguration, stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||
let scope: SearchStickersScope
|
||||
switch stickerSettings.emojiStickerSuggestionMode {
|
||||
case .none:
|
||||
scope = []
|
||||
case .all:
|
||||
scope = [.installed, .remote]
|
||||
if stickerConfiguration.disableLocalSuggestions {
|
||||
scope = [.remote]
|
||||
} else {
|
||||
scope = [.installed, .remote]
|
||||
}
|
||||
case .installed:
|
||||
scope = [.installed]
|
||||
}
|
||||
|
@ -936,6 +936,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
var displayLeave = !channel.flags.contains(.isCreator)
|
||||
var canViewStats = false
|
||||
var hasDiscussion = false
|
||||
var hasVoiceChat = false
|
||||
if let cachedChannelData = cachedData as? CachedChannelData {
|
||||
canViewStats = cachedChannelData.flags.contains(.canViewStats)
|
||||
}
|
||||
@ -952,6 +953,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
|
||||
result.append(.addMember)
|
||||
}
|
||||
if channel.flags.contains(.hasVoiceChat) {
|
||||
hasVoiceChat = true
|
||||
}
|
||||
}
|
||||
switch channel.participationStatus {
|
||||
case .member:
|
||||
@ -963,6 +967,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
displayLeave = false
|
||||
}
|
||||
result.append(.mute)
|
||||
if hasVoiceChat {
|
||||
result.append(.voiceChat)
|
||||
}
|
||||
if hasDiscussion {
|
||||
result.append(.discussion)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ enum PeerInfoHeaderButtonKey: Hashable {
|
||||
case discussion
|
||||
case call
|
||||
case videoCall
|
||||
case voiceChat
|
||||
case mute
|
||||
case more
|
||||
case addMember
|
||||
@ -36,6 +37,7 @@ enum PeerInfoHeaderButtonIcon {
|
||||
case message
|
||||
case call
|
||||
case videoCall
|
||||
case voiceChat
|
||||
case mute
|
||||
case unmute
|
||||
case more
|
||||
@ -112,6 +114,8 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
||||
imageName = "Peer Info/ButtonCall"
|
||||
case .videoCall:
|
||||
imageName = "Peer Info/ButtonVideo"
|
||||
case .voiceChat:
|
||||
imageName = "Peer Info/ButtonVoiceChat"
|
||||
case .mute:
|
||||
imageName = "Peer Info/ButtonMute"
|
||||
case .unmute:
|
||||
@ -3273,6 +3277,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
case .videoCall:
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonVideoCall
|
||||
buttonIcon = .videoCall
|
||||
case .voiceChat:
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonVoiceChat
|
||||
buttonIcon = .voiceChat
|
||||
case .mute:
|
||||
if let notificationSettings = notificationSettings, case .muted = notificationSettings.muteState {
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonUnmute
|
||||
|
@ -2850,6 +2850,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.requestCall(isVideo: false)
|
||||
case .videoCall:
|
||||
self.requestCall(isVideo: true)
|
||||
case .voiceChat:
|
||||
if let cachedData = self.data?.cachedData as? CachedChannelData, let activeCall = cachedData.activeCall {
|
||||
self.context.joinGroupCall(peerId: self.peerId, activeCall: activeCall)
|
||||
}
|
||||
case .mute:
|
||||
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
||||
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
||||
|
Loading…
x
Reference in New Issue
Block a user