import Foundation import UIKit import Display import ContextUI import TelegramCore import SwiftSignalKit import DeleteChatPeerActionSheetItem import PeerListItemComponent import LegacyComponents import LegacyUI import WebSearchUI import MapResourceToAvatarSizes import LegacyMediaPickerUI import AvatarNode import PresentationDataUtils import AccountContext import CallsEmoji import AlertComponent import TelegramPresentationData import ComponentFlow import MultilineTextComponent private func resolvedEmojiKey(data: Data) -> [String] { let resolvedKey = stringForEmojiHashOfData(data, 4) ?? [] return resolvedKey } private final class EmojiKeyAlertComponet: CombinedComponent { let theme: PresentationTheme let emojiKey: [String] let title: String let text: String init(theme: PresentationTheme, emojiKey: [String], title: String, text: String) { self.theme = theme self.emojiKey = emojiKey self.title = title self.text = text } static func ==(lhs: EmojiKeyAlertComponet, rhs: EmojiKeyAlertComponet) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.emojiKey != rhs.emojiKey { return false } if lhs.title != rhs.title { return false } if lhs.text != rhs.text { return false } return true } public static var body: Body { //let emojiKeyItems = ChildMap(environment: MultilineTextComponent.self, keyedBy: Int.self) let emojiKey = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) return { context in /*let emojiKeyItems = context.component.emojiKey.map { item in return emojiKeyItems[item].update( component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: context.component.emojiKey.joined(separator: ""), font: Font.semibold(40.0), textColor: context.component.theme.actionSheet.primaryTextColor)), horizontalAlignment: .center )), environment: {}, availableSize: CGSize(width: 100.0, height: 100.0), transition: .immediate ) }*/ let emojiKey = emojiKey.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: context.component.emojiKey.joined(separator: ""), font: Font.semibold(40.0), textColor: context.component.theme.actionSheet.primaryTextColor)), horizontalAlignment: .center ), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: .immediate ) let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: context.component.title, font: Font.semibold(16.0), textColor: context.component.theme.actionSheet.primaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: .immediate ) let text = text.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: context.component.text, font: Font.regular(13.0), textColor: context.component.theme.actionSheet.primaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width, height: 10000.0), transition: .immediate ) var size = CGSize(width: 0.0, height: 0.0) size.width = max(size.width, emojiKey.size.width) size.width = max(size.width, title.size.width) size.width = max(size.width, text.size.width) let titleSpacing: CGFloat = 10.0 let textSpacing: CGFloat = 10.0 size.height += emojiKey.size.height size.height += titleSpacing size.height += title.size.height size.height += textSpacing size.height += text.size.height var contentHeight: CGFloat = 0.0 let emojiKeyFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiKey.size.width) * 0.5), y: contentHeight), size: emojiKey.size) contentHeight += emojiKey.size.height + titleSpacing let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - title.size.width) * 0.5), y: contentHeight), size: title.size) contentHeight += title.size.height + textSpacing let textFrame = CGRect(origin: CGPoint(x: floor((size.width - text.size.width) * 0.5), y: contentHeight), size: text.size) contentHeight += text.size.height + 5.0 context.add(emojiKey .position(emojiKeyFrame.center) ) context.add(title .position(titleFrame.center) ) context.add(text .position(textFrame.center) ) return size } } } extension VideoChatScreenComponent.View { func openMoreMenu() { guard let sourceView = self.navigationLeftButton.view else { return } guard let environment = self.environment, let controller = environment.controller() else { return } guard let currentCall = self.currentCall else { return } guard let callState = self.callState else { return } let canManageCall = callState.canManageCall var items: [ContextMenuItem] = [] if self.peer != nil, let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 { for peer in displayAsPeers { if peer.peer.id == callState.myPeerId { let avatarSize = CGSize(width: 28.0, height: 28.0) items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in guard let self else { return } c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuDisplayAsItems())))) }))) items.append(.separator) break } } } /*if case let .group(groupCall) = currentCall, let encryptionKey = groupCall.encryptionKeyValue { //TODO:localize let emojiKey = resolvedEmojiKey(data: encryptionKey) items.append(.action(ContextMenuActionItem(text: "Encryption Key", textLayout: .secondLineWithValue(emojiKey.joined(separator: "")), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in c?.dismiss(completion: nil) guard let self, let environment = self.environment else { return } let alertController = componentAlertController( theme: AlertControllerTheme(presentationTheme: defaultDarkPresentationTheme, fontSize: .regular), content: AnyComponent(EmojiKeyAlertComponet( theme: defaultDarkPresentationTheme, emojiKey: emojiKey, title: "This call is end-to-end encrypted", text: "If the emojis on everyone's screens are the same, this call is 100% secure." )), actions: [ComponentAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})], actionLayout: .horizontal ) environment.controller()?.present(alertController, in: .window(.root)) }))) items.append(.separator) }*/ if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { var currentOutputTitle = "" for output in availableOutputs { if output == currentOutput { let title: String switch output { case .builtin: title = UIDevice.current.model case .speaker: title = environment.strings.Call_AudioRouteSpeaker case .headphones: title = environment.strings.Call_AudioRouteHeadphones case let .port(port): title = port.name } currentOutputTitle = title break } } items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in guard let self else { return } c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuAudioItems())))) }))) } if canManageCall { let text: String if case let .channel(channel) = peer, case .broadcast = channel.info { text = environment.strings.LiveStream_EditTitle } else { text = environment.strings.VoiceChat_EditTitle } items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) guard let self else { return } self.openTitleEditing() }))) var hasPermissions = true if let peer = self.peer, case let .channel(chatPeer) = peer { if case .broadcast = chatPeer.info { hasPermissions = false } else if chatPeer.flags.contains(.isGigagroup) { hasPermissions = false } } if hasPermissions { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in guard let self else { return } c?.pushItems(items: .single(ContextController.Items(content: .list(self.contextMenuPermissionItems())))) }))) } } if let inviteLinks = self.inviteLinks { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) guard let self else { return } self.presentShare(inviteLinks) }))) } //let isScheduled = strongSelf.isScheduled let isScheduled: Bool = !"".isEmpty let canSpeak: Bool if let muteState = callState.muteState { canSpeak = muteState.canUnmute } else { canSpeak = true } if !isScheduled && canSpeak { if #available(iOS 15.0, *) { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) AVCaptureDevice.showSystemUserInterface(.microphoneModes) }))) } } if let members = self.members, members.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { let qualityList: [(Int, String)] = [ (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly), (180, "180p"), (360, "360p"), (Int.max, "720p") ] let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in return nil }, action: { [weak self] c, _ in guard let self else { c?.dismiss(completion: nil) return } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) }, iconPosition: .left, action: { (c, _) in c?.popItems() }))) items.append(.separator) for (quality, title) in qualityList { let isSelected = self.maxVideoQuality == quality items.append(.action(ContextMenuActionItem(text: title, icon: { _ in if isSelected { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) } else { return nil } }, action: { [weak self] _, f in f(.default) guard let self else { return } if self.maxVideoQuality != quality { self.maxVideoQuality = quality self.state?.updated(transition: .immediate) } }))) } c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) }))) } if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { if currentCall.hasScreencast { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) guard let self, let currentCall = self.currentCall else { return } currentCall.disableScreencast() }))) } else { items.append(.custom(VoiceChatShareScreenContextItem(context: currentCall.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) }, action: { _, _ in }), false)) } } if canManageCall { if let recordingStartTimestamp = callState.recordingStartTimestamp { items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } let alertController = textAlertController(context: currentCall.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } groupCall.setShouldBeRecording(false, title: nil, videoOrientation: nil) Queue.mainQueue().after(0.88) { HapticFeedback().success() } let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_RecordingSaved } else { text = environment.strings.VideoChat_RecordingSaved } self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in if case .info = value, let self, let environment = self.environment, let currentCall = self.currentCall, let navigationController = environment.controller()?.navigationController as? NavigationController { let context = currentCall.accountContext environment.controller()?.dismiss(completion: { [weak navigationController] in Queue.mainQueue().justDispatch { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer, let navigationController else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) }) } }) return true } return false }) })]) environment.controller()?.present(alertController, in: .window(.root)) }), false)) } else { let text: String if case let .channel(channel) = peer, case .broadcast = channel.info { text = environment.strings.LiveStream_StartRecording } else { text = environment.strings.VoiceChat_StartRecording } if callState.scheduleTimestamp == nil { items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, let environment = self.environment, let currentCall = self.currentCall, let peer = self.peer else { return } let controller = VoiceChatRecordingSetupController(context: currentCall.accountContext, peer: peer, completion: { [weak self] videoOrientation in guard let self, let environment = self.environment, let currentCall = self.currentCall, let peer = self.peer else { return } let title: String let text: String let placeholder: String if let _ = videoOrientation { placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholderVideo } else { placeholder = environment.strings.VoiceChat_RecordingTitlePlaceholder } if case let .channel(channel) = peer, case .broadcast = channel.info { title = environment.strings.LiveStream_StartRecordingTitle if let _ = videoOrientation { text = environment.strings.LiveStream_StartRecordingTextVideo } else { text = environment.strings.LiveStream_StartRecordingText } } else { title = environment.strings.VoiceChat_StartRecordingTitle if let _ = videoOrientation { text = environment.strings.VoiceChat_StartRecordingTextVideo } else { text = environment.strings.VoiceChat_StartRecordingText } } let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let peer = self.peer, let title else { return } groupCall.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) let text: String if case let .channel(channel) = peer, case .broadcast = channel.info { text = environment.strings.LiveStream_RecordingStarted } else { text = environment.strings.VoiceChat_RecordingStarted } self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) groupCall.playTone(.recordingStarted) }) environment.controller()?.present(controller, in: .window(.root)) }) environment.controller()?.present(controller, in: .window(.root)) }))) } } } if canManageCall { let text: String if case let .channel(channel) = peer, case .broadcast = channel.info { text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream } else { text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat } items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } let action: () -> Void = { [weak self] in guard let self, let currentCall = self.currentCall else { return } switch currentCall { case let .group(groupCall): let _ = (groupCall.leave(terminateIfPossible: true) |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let environment = self.environment else { return } environment.controller()?.dismiss() }) case let .conferenceSource(conferenceSource): let _ = (conferenceSource.hangUp() |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let environment = self.environment else { return } environment.controller()?.dismiss() }) } } let title: String let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText } else { title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText } let alertController = textAlertController(context: currentCall.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { action() })]) environment.controller()?.present(alertController, in: .window(.root)) }))) } else { let leaveText: String if case let .channel(channel) = peer, case .broadcast = channel.info { leaveText = environment.strings.LiveStream_LeaveVoiceChat } else { leaveText = environment.strings.VoiceChat_LeaveVoiceChat } items.append(.action(ContextMenuActionItem(text: leaveText, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, let currentCall = self.currentCall else { return } switch currentCall { case let .group(groupCall): let _ = (groupCall.leave(terminateIfPossible: false) |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let environment = self.environment else { return } environment.controller()?.dismiss() }) case let .conferenceSource(conferenceSource): let _ = (conferenceSource.hangUp() |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let environment = self.environment else { return } environment.controller()?.dismiss() }) } }))) } let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } private func contextMenuDisplayAsItems() -> [ContextMenuItem] { guard let environment = self.environment else { return [] } guard case let .group(groupCall) = self.currentCall else { return [] } guard let callState = self.callState else { return [] } let myPeerId = callState.myPeerId let avatarSize = CGSize(width: 28.0, height: 28.0) var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) }, iconPosition: .left, action: { (c, _) in c?.popItems() }))) items.append(.separator) var isGroup = false if let displayAsPeers = self.displayAsPeers { for peer in displayAsPeers { if peer.peer is TelegramGroup { isGroup = true break } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { isGroup = true break } } } items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? environment.strings.VoiceChat_DisplayAsInfoGroup : environment.strings.VoiceChat_DisplayAsInfo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) }), true)) if let displayAsPeers = self.displayAsPeers { for peer in displayAsPeers { var subtitle: String? if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = environment.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = environment.strings.Conversation_StatusMembers(subscribers) } } let isSelected = peer.peer.id == myPeerId let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) let theme = environment.theme let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) |> map { image -> UIImage? in if isSelected, let image = image { return generateImage(extendedAvatarSize, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) let lineWidth = 1.0 + UIScreenPixel context.setLineWidth(lineWidth) context.setStrokeColor(theme.actionSheet.controlAccentColor.cgColor) context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) }) } else { return image } } items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in f(.default) guard let self, case let .group(groupCall) = self.currentCall else { return } if peer.peer.id != myPeerId { groupCall.reconnect(as: peer.peer.id) } }))) if peer.peer.id.namespace == Namespaces.Peer.CloudUser { items.append(.separator) } } } return items } private func contextMenuAudioItems() -> [ContextMenuItem] { guard let environment = self.environment else { return [] } guard let (availableOutputs, currentOutput) = self.audioOutputState else { return [] } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) }, iconPosition: .left, action: { (c, _) in c?.popItems() }))) items.append(.separator) for output in availableOutputs { let title: String switch output { case .builtin: title = UIDevice.current.model case .speaker: title = environment.strings.Call_AudioRouteSpeaker case .headphones: title = environment.strings.Call_AudioRouteHeadphones case let .port(port): title = port.name } items.append(.action(ContextMenuActionItem(text: title, icon: { theme in if output == currentOutput { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) } else { return nil } }, action: { [weak self] _, f in f(.default) guard let self, let currentCall = self.currentCall else { return } currentCall.setCurrentAudioOutput(output) }))) } return items } private func contextMenuPermissionItems() -> [ContextMenuItem] { guard let environment = self.environment, let callState = self.callState else { return [] } var items: [ContextMenuItem] = [] if callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { let isMuted = defaultParticipantMuteState == .muted items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) }, iconPosition: .left, action: { (c, _) in c?.popItems() }))) items.append(.separator) items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in if isMuted { return nil } else { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) } }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, case let .group(groupCall) = self.currentCall else { return } groupCall.updateDefaultParticipantsAreMuted(isMuted: false) }))) items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in if !isMuted { return nil } else { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) } }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self, case let .group(groupCall) = self.currentCall else { return } groupCall.updateDefaultParticipantsAreMuted(isMuted: true) }))) } return items } }