import Foundation import UIKit import Display import SwiftSignalKit import AccountContext import TelegramCore import ContextUI import DeleteChatPeerActionSheetItem import UndoUI import LegacyComponents import WebSearchUI import MapResourceToAvatarSizes import LegacyUI import LegacyMediaPickerUI extension VideoChatScreenComponent.View { func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { guard let environment = self.environment else { return } guard let members = self.members, let participant = members.participants.first(where: { $0.id == .peer(id) }) else { return } guard let currentCall = self.currentCall else { return } let muteStatePromise = Promise(participant.muteState) let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in guard let self, let environment = self.environment, let currentCall = self.currentCall else { return [] } guard let callState = self.callState else { return [] } guard let peer = participant.peer else { return [] } var items: [ContextMenuItem] = [] var hasVolumeSlider = false if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { } else { if callState.canManageCall || callState.myPeerId != id { hasVolumeSlider = true let minValue: CGFloat if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { minValue = 0.01 } else { minValue = 0.0 } items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in guard let self, case let .group(groupCall) = self.currentCall else { return } if finished && newValue.isZero { let updatedMuteState = groupCall.updateMuteState(peerId: peer.id, isMuted: true) muteStatePromise.set(.single(updatedMuteState)) } else { groupCall.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) } }), true)) } } if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer?.smallProfileImage == nil) { items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) }), true)) } if peer.id == callState.myPeerId { if participant.hasRaiseHand { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, case let .group(groupCall) = self.currentCall else { return } groupCall.lowerHand() f(.default) }))) } items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) Queue.mainQueue().after(0.1) { guard let self else { return } self.openAvatarForEditing(fromGallery: false, completion: {}) } }))) items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) Queue.mainQueue().after(0.1) { guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } let maxBioLength: Int if peer.id.namespace == Namespaces.Peer.CloudUser { maxBioLength = 70 } else { maxBioLength = 100 } let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in guard let self, let environment = self.environment, let currentCall = self.currentCall, let bio else { return } if peer.id.namespace == Namespaces.Peer.CloudUser { let _ = (currentCall.accountContext.engine.accountData.updateAbout(about: bio) |> `catch` { _ -> Signal in return .complete() }).start() } else { let _ = (currentCall.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) |> `catch` { _ -> Signal in return .complete() }).start() } self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) }) environment.controller()?.present(controller, in: .window(.root)) } }))) if case let .user(peer) = peer { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) Queue.mainQueue().after(0.1) { guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } let controller = voiceChatUserNameController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in guard let self, let environment = self.environment, let currentCall = self.currentCall, let (firstName, lastName) = firstAndLastName else { return } let _ = currentCall.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) }) environment.controller()?.present(controller, in: .window(.root)) } }))) } } else { if (callState.canManageCall || callState.adminIds.contains(currentCall.accountContext.account.peerId)) { if callState.adminIds.contains(peer.id) { if let _ = muteState { } else { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, case let .group(groupCall) = self.currentCall else { return } let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true) f(.default) }))) } } else { if let muteState = muteState, !muteState.canUnmute { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false) f(.default) if let participantPeer = participant.peer { self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) } }))) } else { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, case let .group(groupCall) = self.currentCall else { return } let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true) f(.default) }))) } } } else { if let muteState = muteState, muteState.mutedByYou { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, case let .group(groupCall) = self.currentCall else { return } let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false) f(.default) }))) } else { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, case let .group(groupCall) = self.currentCall else { return } let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true) f(.default) }))) } } let openTitle: String let openIcon: UIImage? if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { if case let .channel(peer) = peer, case .broadcast = peer.info { openTitle = environment.strings.VoiceChat_OpenChannel openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") } else { openTitle = environment.strings.VoiceChat_OpenGroup openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") } } else { openTitle = environment.strings.Conversation_ContextMenuSendMessage openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") } items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { return } let context = currentCall.accountContext controller.dismiss(completion: { [weak navigationController] in Queue.mainQueue().after(0.1) { guard let navigationController else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) } }) f(.dismissWithoutContent) }))) if case let .group(groupCall) = self.currentCall, (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != groupCall.peerId { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self, case let .group(groupCall) = self.currentCall else { return } let chatPeer: Signal if let peerId = groupCall.peerId { chatPeer = groupCall.accountContext.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) } else { chatPeer = .single(nil) } let _ = (chatPeer |> deliverOnMainQueue).start(next: { [weak self] chatPeer in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] let nameDisplayOrder = presentationData.nameDisplayOrder if let chatPeer { items.append(DeleteChatPeerActionSheetItem(context: groupCall.accountContext, peer: peer, chatPeer: chatPeer, action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) } else { items.append(ActionSheetTextItem(title: environment.strings.VoiceChat_RemoveConferencePeerConfirmation(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, parseMarkdown: true)) } items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } if groupCall.isConference { groupCall.kickPeer(id: peer.id) } else { if let callPeerId = groupCall.peerId { let _ = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: groupCall.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() groupCall.removedPeer(peer.id) } } self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) })) actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) environment.controller()?.present(actionSheet, in: .window(.root)) }) }) }))) } } return items } let items = muteStatePromise.get() |> map { muteState -> [ContextMenuItem] in return itemsForEntry(muteState) } let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let contextController = ContextController( presentationData: presentationData, source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), items: items |> map { items in return ContextController.Items(content: .list(items)) }, recognizer: nil, gesture: gesture ) environment.controller()?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismiss() } return true }) environment.controller()?.presentInGlobalOverlay(contextController) } func openInvitedParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { guard let environment = self.environment else { return } guard let currentCall = self.currentCall else { return } guard case .group = self.currentCall else { return } let itemsForEntry: () -> [ContextMenuItem] = { [weak self] in guard let self, let environment = self.environment else { return [] } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] c, _ in c?.dismiss(result: .dismissWithoutContent, completion: nil) guard let self else { return } guard case let .group(groupCall) = self.currentCall else { return } groupCall.kickPeer(id: id) }))) return items } let items = itemsForEntry() let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let contextController = ContextController( presentationData: presentationData, source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture ) environment.controller()?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismiss() } return true }) environment.controller()?.presentInGlobalOverlay(contextController) } private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { guard let currentCall = self.currentCall else { return } guard let callState = self.callState else { return } let peerId = callState.myPeerId let _ = (currentCall.accountContext.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Configuration.SearchBots() ) |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in guard let self, let currentCall = self.currentCall, let environment = self.environment else { return } guard let peer else { return } let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let legacyController = LegacyController(presentation: .custom, theme: environment.theme) legacyController.statusBar.statusBarStyle = .Ignore let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) legacyController.bind(controller: navigationController) self.endEditing(true) environment.controller()?.present(legacyController, in: .window(.root)) var hasPhotos = false if !peer.profileImageRepresentations.isEmpty { hasPhotos = true } let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! mixin.forceDark = true mixin.stickersContext = LegacyPaintStickersContext(context: currentCall.accountContext) let _ = self.currentAvatarMixin.swap(mixin) mixin.requestSearchController = { [weak self] assetsController in guard let self, let currentCall = self.currentCall, let environment = self.environment else { return } let controller = WebSearchController(context: currentCall.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in assetsController?.dismiss() guard let self else { return } self.updateProfilePhoto(result) })) controller.navigationPresentation = .modal environment.controller()?.push(controller) if fromGallery { completion() } } mixin.didFinishWithImage = { [weak self] image in if let image = image { completion() self?.updateProfilePhoto(image) } } mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in if let image = image, let asset = asset { completion() self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) } } mixin.didFinishWithDelete = { [weak self] in guard let self, let environment = self.environment else { return } let proceed = { [weak self] in guard let self, let currentCall = self.currentCall else { return } let _ = self.currentAvatarMixin.swap(nil) let postbox = currentCall.accountContext.account.postbox self.updateAvatarDisposable.set((currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start()) } let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() proceed() }) ] actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) environment.controller()?.present(actionSheet, in: .window(.root)) } mixin.didDismiss = { [weak self, weak legacyController] in guard let self else { return } let _ = self.currentAvatarMixin.swap(nil) legacyController?.dismiss() } let menuController = mixin.present() if let menuController = menuController { menuController.customRemoveFromParentViewController = { [weak legacyController] in legacyController?.dismiss() } } }) } private func updateProfilePhoto(_ image: UIImage) { guard let currentCall = self.currentCall else { return } guard let callState = self.callState else { return } guard let data = image.jpegData(compressionQuality: 0.6) else { return } let peerId = callState.myPeerId let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) currentCall.accountContext.account.postbox.mediaBox.storeResourceData(resource.id, data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = (representation, 0.0) let postbox = currentCall.accountContext.account.postbox let signal = peerId.namespace == Namespaces.Peer.CloudUser ? currentCall.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) : currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: currentCall.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) self.updateAvatarDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } switch result { case .complete: self.currentUpdatingAvatar = nil self.state?.updated(transition: .spring(duration: 0.4)) case let .progress(value): self.currentUpdatingAvatar = (representation, value) } })) self.state?.updated(transition: .spring(duration: 0.4)) } private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { guard let currentCall = self.currentCall else { return } guard let callState = self.callState else { return } guard let data = image.jpegData(compressionQuality: 0.6) else { return } let peerId = callState.myPeerId let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) currentCall.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = (representation, 0.0) var videoStartTimestamp: Double? = nil if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue } let context = currentCall.accountContext let account = context.account let signal = Signal { [weak self] subscriber in let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in if let paintingData = adjustments.paintingData, paintingData.hasAnimation { return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) } else { return nil } } let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { let durationSignal: SSignal = SSignal(generator: { subscriber in let disposable = (entityRenderer.duration()).start(next: { duration in subscriber.putNext(duration) subscriber.putCompletion() }) return SBlockDisposable(block: { disposable.dispose() }) }) signal = durationSignal.map(toSignal: { duration -> SSignal in if let duration = duration as? Double { return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! } else { return SSignal.single(nil) } }) } else if let asset = asset as? AVAsset { signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! } else { signal = SSignal.complete() } let signalDisposable = signal.start(next: { next in if let result = next as? TGMediaVideoConversionResult { if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) } if let timestamp = videoStartTimestamp { videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) } var value = stat() if stat(result.fileURL.path, &value) == 0 { if let data = try? Data(contentsOf: result.fileURL) { let resource: TelegramMediaResource if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { resource = LocalFileMediaResource(fileId: liveUploadData.id) } else { resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) } account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) EngineTempBox.shared.dispose(tempFile) } } subscriber.putCompletion() } else if let progress = next as? NSNumber { Queue.mainQueue().async { [weak self] in guard let self else { return } self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25) self.state?.updated(transition: .spring(duration: 0.4)) } } }, error: { _ in }, completed: nil) let disposable = ActionDisposable { signalDisposable?.dispose() } return ActionDisposable { disposable.dispose() } } self.updateAvatarDisposable.set((signal |> mapToSignal { videoResource -> Signal in if peerId.namespace == Namespaces.Peer.CloudUser { return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else { return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } } |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } switch result { case .complete: self.currentUpdatingAvatar = nil self.state?.updated(transition: .spring(duration: 0.4)) case let .progress(value): self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75) self.state?.updated(transition: .spring(duration: 0.4)) } })) } } private final class ParticipantExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = false let blurBackground: Bool = true private let contentView: ContextExtractedContentContainingView init(contentView: ContextExtractedContentContainingView) { self.contentView = contentView } func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) } func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } }