From a2234e9271900192390598197084bebababea734 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 8 Apr 2025 20:17:13 +0400 Subject: [PATCH] Conference updates --- .../ContactMultiselectionController.swift | 1 + .../CallListUI/Sources/CallListCallItem.swift | 2 +- .../Sources/CallListController.swift | 15 +- .../ChatMessageCallBubbleContentNode.swift | 2 +- .../ContactMultiselectionController.swift | 7 + .../ContactMultiselectionControllerNode.swift | 130 ++++++++++++++++-- .../Sources/SharedAccountContext.swift | 5 +- 7 files changed, 144 insertions(+), 18 deletions(-) diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 43f815e7d0..6126ff1075 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -157,4 +157,5 @@ public protocol ContactMultiselectionController: ViewController { var result: Signal { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } + var isCallVideoOptionSelected: Bool { get } } diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index a949037d2f..a408a63cbc 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -704,7 +704,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.conferenceAvatarListNode = conferenceAvatarListNode strongSelf.containerNode.addSubnode(conferenceAvatarListNode) } - let avatarListContents = conferenceAvatarListContext.update(peers: conferenceAvatars, animated: false) + let avatarListContents = conferenceAvatarListContext.update(peers: Array(conferenceAvatars.prefix(3)), animated: false) let avatarListSize = conferenceAvatarListNode.update(context: item.context, content: avatarListContents, itemSize: CGSize(width: CGFloat(multipleAvatarDiameter), height: CGFloat(multipleAvatarDiameter)), customSpacing: multipleAvatarDiameter - 8.0, font: multipleAvatarFont, animated: false, synchronousLoad: synchronousLoads) conferenceAvatarListNode.frame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - avatarListSize.width) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - avatarListSize.height) / 2.0)), size: avatarListSize) } else { diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 0607e99d09..1acf78752c 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -208,7 +208,7 @@ public final class CallListController: TelegramBaseController { } } - private func createGroupCall(peerIds: [EnginePeer.Id], completion: (() -> Void)? = nil) { + private func createGroupCall(peerIds: [EnginePeer.Id], isVideo: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) guard !self.presentAccountFrozenInfoIfNeeded() else { @@ -274,7 +274,7 @@ public final class CallListController: TelegramBaseController { isStream: false ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), - beginWithVideo: false, + beginWithVideo: isVideo, invitePeerIds: peerIds ) completion?() @@ -401,7 +401,7 @@ public final class CallListController: TelegramBaseController { } }, createGroupCall: { [weak self] in if let strongSelf = self { - strongSelf.createGroupCall(peerIds: []) + strongSelf.createGroupCall(peerIds: [], isVideo: false) } }) @@ -520,7 +520,7 @@ public final class CallListController: TelegramBaseController { guard let self else { return } - self.createGroupCall(peerIds: []) + self.createGroupCall(peerIds: [], isVideo: false) }, clearHighlightAutomatically: true)] let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( @@ -565,11 +565,12 @@ public final class CallListController: TelegramBaseController { controller?.dismiss() return } + + let isVideo = controller?.isCallVideoOptionSelected ?? false if peerIds.count == 1 { - //TODO:release isVideo controller?.dismiss() - self.call(peerIds[0], isVideo: false, began: { [weak self] in + self.call(peerIds[0], isVideo: isVideo, began: { [weak self] in if let strongSelf = self { let _ = (strongSelf.context.sharedContext.hasOngoingCall.get() |> filter { $0 } @@ -586,7 +587,7 @@ public final class CallListController: TelegramBaseController { } }) } else { - self.createGroupCall(peerIds: peerIds, completion: { + self.createGroupCall(peerIds: peerIds, isVideo: isVideo, completion: { controller?.dismiss() }) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 8f414535fd..03136f8bde 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -323,7 +323,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.addSubnode(peopleAvatarsNode) } - let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.map(EnginePeer.init), animated: false) + let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.prefix(3).map(EnginePeer.init), animated: false) let peopleAvatarsSize = peopleAvatarsNode.update(context: item.context, content: peopleAvatarsContent, itemSize: CGSize(width: peopleAvatarSize, height: peopleAvatarSize), customSpacing: peopleAvatarSize - peopleAvatarSpacing, font: avatarFont, animated: false, synchronousLoad: false) peopleAvatarsNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsLeftInset, y: labelFrame.minY - 1.0), size: peopleAvatarsSize) } else { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index e97a1232a6..31ffa8fc5f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -86,6 +86,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection private let onlyWriteable: Bool private let isGroupInvitation: Bool private let limit: Int32? + + public var isCallVideoOptionSelected: Bool { + guard self.displayNode.isNodeLoaded, let displayNode = self.displayNode as? ContactMultiselectionControllerNode else { + return false + } + return displayNode.isCallVideoOptionSelected + } init(_ params: ContactMultiselectionControllerParams) { self.params = params diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index e555404005..58fa17ca74 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -15,6 +15,8 @@ import EditableTokenListNode import SolidRoundedButtonNode import ContextUI import ComponentFlow +import MultilineTextComponent +import CheckComponent private struct SearchResultEntry: Identifiable { let index: Int @@ -84,7 +86,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let onlyWriteable: Bool private let isGroupInvitation: Bool - private var bottomPanel: ComponentView? + var isCallVideoOptionSelected: Bool { + return self.footerPanelNode?.isCheckOptionSelected ?? false + } init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar @@ -119,19 +123,19 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { shortPlaceholder = self.presentationData.strings.Common_Search self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: nil) case .requestedUsersSelection: placeholder = self.presentationData.strings.RequestPeer_SelectUsers_SearchPlaceholder self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: nil) case let .groupCreation(isCall): if isCall { //TODO:localize placeholder = "Search for contacts or usernames" self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: isCall ? "Call with video enabled" : nil) } else { placeholder = self.presentationData.strings.Compose_TokenListPlaceholder self.footerPanelNode = nil @@ -554,9 +558,16 @@ private final class FooterPanelNode: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings + + private let checkOptionTitle: String? + private var checkOptionButton: HighlightTrackingButton? + private var checkOptionText: ComponentView? + private var checkOptionControl: ComponentView? private let separatorNode: ASDisplayNode private let button: SolidRoundedButtonView + + private(set) var isCheckOptionSelected: Bool = false private var validLayout: (CGFloat, CGFloat, CGFloat)? @@ -573,14 +584,15 @@ private final class FooterPanelNode: ASDisplayNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, checkOptionTitle: String?) { self.theme = theme self.strings = strings + self.checkOptionTitle = checkOptionTitle self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor - self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) + self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 10.0) self.content = Content(title: self.strings.Premium_Gift_ContactSelection_Proceed, badge: "") @@ -599,9 +611,17 @@ private final class FooterPanelNode: ASDisplayNode { super.didLoad() self.view.addSubview(self.button) } + + @objc private func checkOptionButtonPressed() { + self.isCheckOptionSelected = !self.isCheckOptionSelected + if let validLayout = self.validLayout { + let _ = self.updateLayout(width: validLayout.0, sideInset: validLayout.1, bottomInset: validLayout.2, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 9.0 var bottomInset = bottomInset bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) @@ -609,10 +629,104 @@ private final class FooterPanelNode: ASDisplayNode { let buttonInset: CGFloat = 16.0 + sideInset let buttonWidth = width - buttonInset * 2.0 let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) - transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - return topInset + buttonHeight + bottomInset + var height = topInset + buttonHeight + bottomInset + + var buttonOffset: CGFloat = 0.0 + if let checkOptionTitle = self.checkOptionTitle { + let checkSpacing: CGFloat = 10.0 + + let checkOptionButton: HighlightTrackingButton + if let current = self.checkOptionButton { + checkOptionButton = current + } else { + checkOptionButton = HighlightTrackingButton() + self.checkOptionButton = checkOptionButton + self.view.addSubview(checkOptionButton) + checkOptionButton.addTarget(self, action: #selector(self.checkOptionButtonPressed), for: .touchUpInside) + } + + let checkOptionText: ComponentView + if let current = self.checkOptionText { + checkOptionText = current + } else { + checkOptionText = ComponentView() + self.checkOptionText = checkOptionText + } + + let checkOptionControl: ComponentView + if let current = self.checkOptionControl { + checkOptionControl = current + } else { + checkOptionControl = ComponentView() + self.checkOptionControl = checkOptionControl + } + + let checkOptionTextSize = checkOptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: checkOptionTitle, font: Font.regular(13.0), textColor: theme.rootController.navigationBar.primaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0 - checkSpacing - 20.0, height: 100.0) + ) + + let checkTheme = CheckComponent.Theme( + backgroundColor: self.theme.list.itemCheckColors.fillColor, + strokeColor: self.theme.list.itemCheckColors.foregroundColor, + borderColor: self.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + let checkOptionControlSize = checkOptionControl.update( + transition: transition.isAnimated ? .easeInOut(duration: 0.2) : .immediate, + component: AnyComponent(CheckComponent( + theme: checkTheme, + size: CGSize(width: 18.0, height: 18.0), + selected: self.isCheckOptionSelected + )), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + + let checkContentWidth = checkOptionControlSize.width + checkSpacing + checkOptionTextSize.width + let checkContentHeight = 49.0 + + let checkOptionControlFrame = CGRect(origin: CGPoint(x: floor((width - checkContentWidth) * 0.5), y: floor(checkContentHeight - checkOptionControlSize.height) * 0.5), size: checkOptionControlSize) + let checkOptionTextFrame = CGRect(origin: CGPoint(x: checkOptionControlFrame.maxX + checkSpacing, y: floor((checkContentHeight - checkOptionTextSize.height) * 0.5)), size: checkOptionTextSize) + + if let checkOptionControlView = checkOptionControl.view { + if checkOptionControlView.superview == nil { + checkOptionControlView.isUserInteractionEnabled = false + checkOptionButton.addSubview(checkOptionControlView) + } + checkOptionControlView.frame = checkOptionControlFrame + } + + if let checkOptionTextView = checkOptionText.view { + if checkOptionTextView.superview == nil { + checkOptionTextView.isUserInteractionEnabled = false + checkOptionButton.addSubview(checkOptionTextView) + } + checkOptionTextView.frame = checkOptionTextFrame + } + + checkOptionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: checkContentHeight)) + + height += checkContentHeight + buttonOffset += checkContentHeight + } else { + if let checkOptionButton = self.checkOptionButton { + self.checkOptionButton = nil + checkOptionButton.removeFromSuperview() + } + } + + transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset + buttonOffset, width: buttonWidth, height: buttonHeight)) + + return height } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d3ec4e7a5a..d3bd0f15bf 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1943,7 +1943,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { let _ = (controller.result |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in + |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak parentController] result in + guard let parentController else { + return + } guard case let .result(rawPeerIds, _) = result else { controller?.dismiss() return