Voice Chat Improvements

This commit is contained in:
Ilya Laktyushin 2021-01-06 03:19:31 +03:00
parent d7694f997c
commit 845ca1e843
26 changed files with 5706 additions and 5440 deletions

View File

@ -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";

View File

@ -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)

View File

@ -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 {

View File

@ -84,7 +84,7 @@ public protocol ContextMenuCustomItem {
public enum ContextMenuItem {
case action(ContextMenuActionItem)
case custom(ContextMenuCustomItem)
case custom(ContextMenuCustomItem, Bool)
case separator
}

View File

@ -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) }

View File

@ -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

View File

@ -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
})
}
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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() {

View File

@ -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

View File

@ -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
))
}
}

View File

@ -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)

View File

@ -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! {

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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]
}

View File

@ -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)
}

View File

@ -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

View File

@ -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()