diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0f732dfa3e..a820b0cc4f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16095,6 +16095,11 @@ Error: %8$@"; "MediaPicker.LivePhoto.Bounce" = "Bounce"; "MediaPicker.LivePhoto.LiveOff" = "Live Off"; +"MediaPicker.LivePhoto.Tooltip.Live" = "Live Photo turned on"; +"MediaPicker.LivePhoto.Tooltip.Loop" = "Live Photo with Loop effect turned on"; +"MediaPicker.LivePhoto.Tooltip.Bounce" = "Live Photo with Bounce effect turned on"; +"MediaPicker.LivePhoto.Tooltip.LiveOff" = "Live Photo turned off"; + "AuthConfirmation.UnverifiedApp" = "Unverified App"; "Chat.ManagedBot.CreatedTitle" = "**%@** is ready!"; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index d84456a51a..f5323777ea 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -132,6 +132,10 @@ typedef NS_ENUM(NSUInteger, TGMediaLivePhotoMode) - (SSignal *)highQualityPhoto; - (void)setHighQualityPhoto:(bool)highQualityPhoto; +- (bool)isForceLivePhotoEnabled; +- (SSignal *)forceLivePhotoEnabled; +- (void)setForceLivePhotoEnabled:(bool)forceLivePhotoEnabled; + - (SSignal *)facesForItem:(NSObject *)item; - (void)setFaces:(NSArray *)faces forItem:(NSObject *)item; diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 626fec405e..dc4f1ada3f 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -144,12 +144,15 @@ SPipe *_captionAbovePipe; SPipe *_highQualityPhotoPipe; SPipe *_livePhotoModePipe; + SPipe *_forceLivePhotoPipe; NSAttributedString *_forcedCaption; bool _captionAbove; bool _highQualityPhoto; + + bool _forceLivePhotoEnabled; } @end @@ -227,6 +230,7 @@ _captionAbovePipe = [[SPipe alloc] init]; _highQualityPhotoPipe = [[SPipe alloc] init]; _livePhotoModePipe = [[SPipe alloc] init]; + _forceLivePhotoPipe = [[SPipe alloc] init]; } return self; } @@ -989,6 +993,27 @@ _highQualityPhotoPipe.sink(@(highQualityPhoto)); } +- (bool)isForceLivePhotoEnabled { + return _forceLivePhotoEnabled; +} + +- (SSignal *)forceLivePhotoEnabled +{ + __weak TGMediaEditingContext *weakSelf = self; + SSignal *updateSignal = [_forceLivePhotoPipe.signalProducer() map:^NSNumber *(NSNumber *update) + { + __strong TGMediaEditingContext *strongSelf = weakSelf; + return @(strongSelf->_forceLivePhotoEnabled); + }]; + + return [[SSignal single:@(_forceLivePhotoEnabled)] then:updateSignal]; +} + +- (void)setForceLivePhotoEnabled:(bool)forceLivePhotoEnabled +{ + _forceLivePhotoEnabled = forceLivePhotoEnabled; + _forceLivePhotoPipe.sink(@(forceLivePhotoEnabled)); +} - (SSignal *)facesForItem:(NSObject *)item { diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 496780dcb7..8088d37de4 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -56,6 +56,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/Components/BundleIconComponent", diff --git a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift index 21d048fa85..6c6c552222 100644 --- a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift +++ b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift @@ -3,6 +3,9 @@ import UIKit import Display import LegacyComponents import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import MultilineTextComponent import GlassBackgroundComponent import ContextUI import TelegramPresentationData @@ -55,21 +58,25 @@ final class LivePhotoButton: UIView, TGLivePhotoButton { var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_LivePhoto_Live, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveOn"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + self?.presentTooltip(.live) self?.modeUpdated?(.live) f(.default) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_LivePhoto_Loop, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveLoop"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + self?.presentTooltip(.loop) self?.modeUpdated?(.loop) f(.default) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_LivePhoto_Bounce, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveBounce"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + self?.presentTooltip(.bounce) self?.modeUpdated?(.bounce) f(.default) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_LivePhoto_LiveOff, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveOff"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + self?.presentTooltip(.off) self?.modeUpdated?(.off) f(.default) @@ -79,6 +86,48 @@ final class LivePhotoButton: UIView, TGLivePhotoButton { self.present?(contextController, nil) } + private func presentTooltip(_ mode: TGMediaLivePhotoMode) { + guard self.mode != mode else { + return + } + let iconName: String + let text: String + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + switch mode { + case .live: + iconName = "Media Editor/LiveLargeOn" + text = presentationData.strings.MediaPicker_LivePhoto_Tooltip_Live + case .loop: + iconName = "Media Editor/LiveLargeLoop" + text = presentationData.strings.MediaPicker_LivePhoto_Tooltip_Loop + case .bounce: + iconName = "Media Editor/LiveLargeBounce" + text = presentationData.strings.MediaPicker_LivePhoto_Tooltip_Bounce + case .off: + iconName = "Media Editor/LiveLargeOff" + text = presentationData.strings.MediaPicker_LivePhoto_Tooltip_LiveOff + default: + iconName = "" + text = "" + } + + let controller = TooltipInfoScreen( + context: self.context, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: iconName, tintColor: .white) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white))) + )) + ], spacing: 11.0)), + alignment: .center + ) + self.present?(controller, nil) + } + func updateFrame(_ frame: CGRect) { let transition: ContainedViewLayoutTransition if self.frame.width.isZero { @@ -185,3 +234,165 @@ private final class LivePhotoReferenceContentSource: ContextReferenceContentSour return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class TooltipInfoContent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let content: AnyComponent + let alignment: TooltipInfoScreen.Alignment + + init( + context: AccountContext, + content: AnyComponent, + alignment: TooltipInfoScreen.Alignment + ) { + self.context = context + self.content = content + self.alignment = alignment + } + + static func ==(lhs: TooltipInfoContent, rhs: TooltipInfoContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.alignment != rhs.alignment { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView = GlassBackgroundView() + private let contentView = ComponentView() + + private var component: TooltipInfoContent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var isDismissed = false + func dismiss() { + guard !self.isDismissed, let controller = self.environment?.controller() else { + return + } + self.isDismissed = true + + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setBlur(layer: self.backgroundView.layer, radius: 10.0) + self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + controller.dismiss() + }) + } + + func update(component: TooltipInfoContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let insets = UIEdgeInsets(top: 12.0, left: 10.0, bottom: 12.0, right: 18.0) + let contentSize = self.contentView.update( + transition: .immediate, + component: component.content, + environment: {}, + containerSize: availableSize + ) + if let contentView = self.contentView.view { + if contentView.superview == nil { + self.backgroundView.contentView.addSubview(contentView) + } + contentView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentSize) + } + + let backgroundSize = CGSize(width: contentSize.width + insets.left + insets.right, height: contentSize.height + insets.top + insets.bottom) + self.backgroundView.update(size: backgroundSize, cornerRadius: backgroundSize.height * 0.5, isDark: true, tintColor: .init(kind: .panel), transition: .immediate) + + let backgroundFrame: CGRect + switch component.alignment { + case .center: + backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundSize.width) / 2.0), y: floor((availableSize.height - backgroundSize.height) / 2.0)), size: backgroundSize) + case .bottom: + backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundSize.width) / 2.0), y: availableSize.height - backgroundSize.height - environment.additionalInsets.bottom - backgroundSize.height - 38.0), size: backgroundSize) + } + + self.backgroundView.frame = backgroundFrame + self.environment = environment + if self.component == nil { + self.backgroundView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + Queue.mainQueue().after(2.5) { + self.dismiss() + } + } + self.component = component + + return availableSize + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + self.dismiss() + + return nil + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TooltipInfoScreen: ViewControllerComponentContainer { + enum Alignment { + case center + case bottom + } + + private let context: AccountContext + + public init( + context: AccountContext, + content: AnyComponent, + alignment: Alignment + ) { + self.context = context + + super.init( + context: context, + component: TooltipInfoContent( + context: context, + content: content, + alignment: alignment + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index c33738cf21..e7e9c914fb 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -111,6 +111,7 @@ final class MediaPickerGridItemNode: GridItemNode { var currentMediaState: (TGMediaSelectableItem, Int)? var currentAssetState: (PHFetchResult, Int)? var currentAsset: PHAsset? + var isLivePhoto = false var currentDraftState: (MediaEditorDraft, Int)? var enableAnimations: Bool = true @@ -175,6 +176,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.typeIconNode.displaysAsynchronously = false self.typeIconNode.displayWithoutProcessing = true self.typeIconNode.isLayerBacked = true + self.typeIconNode.contentMode = .center self.durationNode = ImmediateTextNode() self.durationNode.isLayerBacked = true @@ -547,6 +549,27 @@ final class MediaPickerGridItemNode: GridItemNode { } self.imageNode.setSignal(imageSignal) + if self.currentDraftState != nil { + self.currentDraftState = nil + } + + var typeIcon: UIImage? + var duration: String? + if asset.mediaType == .video { + if asset.mediaSubtypes.contains(.videoHighFrameRate) { + typeIcon = UIImage(bundleImageName: "Media Editor/MediaSlomo") + } else if asset.mediaSubtypes.contains(.videoTimelapse) { + typeIcon = UIImage(bundleImageName: "Media Editor/MediaTimelapse") + } else { + typeIcon = UIImage(bundleImageName: "Media Editor/MediaVideo") + } + duration = stringForDuration(Int32(asset.duration)) + } else { + if asset.mediaSubtypes.contains(.photoLive), self.interaction?.displayLivePhotoIcon == true { + self.isLivePhoto = true + } + } + let spoilerSignal = Signal { subscriber in if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) { let disposable = signal.start(next: { next in @@ -579,35 +602,55 @@ final class MediaPickerGridItemNode: GridItemNode { } } - self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in - guard let strongSelf = self else { + let livePhotoModeSignal: Signal + if self.isLivePhoto { + let itemSignal = Signal { subscriber in + if let signal = editingContext.livePhotoMode(forIdentifier: asset.localIdentifier) { + let disposable = signal.start(next: { next in + subscriber.putNext((next as? UInt).flatMap { TGMediaLivePhotoMode(rawValue: $0) }) + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + let forceSignal = Signal { subscriber in + if let signal = editingContext.forceLivePhotoEnabled() { + let disposable = signal.start(next: { next in + subscriber.putNext((next as? Bool) ?? false) + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + livePhotoModeSignal = combineLatest(itemSignal, forceSignal) + |> map { item, _ in + return item + } + } else { + livePhotoModeSignal = .single(nil) + } + + self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, livePhotoModeSignal, self.selectionPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, livePhotoMode, selectionState in + guard let self else { return } - strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1) + self.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1) + self.updateLivePhotoMode(livePhotoMode) })) - - if self.currentDraftState != nil { - self.currentDraftState = nil - } - - var typeIcon: UIImage? - var duration: String? - if asset.mediaType == .video { - if asset.mediaSubtypes.contains(.videoHighFrameRate) { - typeIcon = UIImage(bundleImageName: "Media Editor/MediaSlomo") - } else if asset.mediaSubtypes.contains(.videoTimelapse) { - typeIcon = UIImage(bundleImageName: "Media Editor/MediaTimelapse") - } else { - typeIcon = UIImage(bundleImageName: "Media Editor/MediaVideo") - } - duration = stringForDuration(Int32(asset.duration)) - } else { - if asset.mediaSubtypes.contains(.photoLive) && interaction.displayLivePhotoIcon { - typeIcon = UIImage(bundleImageName: "Chat/Message/LivePhoto") - } - } - + if asset.isFavorite { typeIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white) } @@ -616,7 +659,7 @@ final class MediaPickerGridItemNode: GridItemNode { if self.leftShadowNode.supernode == nil { self.addSubnode(self.leftShadowNode) } - } else if self.leftShadowNode.supernode != nil { + } else if !self.isLivePhoto, self.leftShadowNode.supernode != nil { self.leftShadowNode.removeFromSupernode() } @@ -633,7 +676,7 @@ final class MediaPickerGridItemNode: GridItemNode { if self.typeIconNode.supernode == nil { self.addSubnode(self.typeIconNode) } - } else if self.typeIconNode.supernode != nil { + } else if !self.isLivePhoto, self.typeIconNode.supernode != nil { self.typeIconNode.removeFromSupernode() } @@ -714,6 +757,48 @@ final class MediaPickerGridItemNode: GridItemNode { } } + private var livePhotoIconName: String? + private func updateLivePhotoMode(_ mode: TGMediaLivePhotoMode?) { + guard self.isLivePhoto, self.interaction?.displayLivePhotoIcon == true else { + return + } + var effectiveMode: TGMediaLivePhotoMode + if let mode, mode != .off { + effectiveMode = mode + } else { + if self.interaction?.editingState.isForceLivePhotoEnabled() == true { + effectiveMode = .live + } else { + effectiveMode = .off + } + } + + var iconName: String + switch effectiveMode { + case .off: + iconName = "Chat/Message/LivePhotoOff" + default: + iconName = "Chat/Message/LivePhoto" + } + + if self.livePhotoIconName != iconName { + self.livePhotoIconName = iconName + if self.leftShadowNode.supernode == nil { + if self.typeIconNode.supernode != nil { + self.insertSubnode(self.leftShadowNode, belowSubnode: self.typeIconNode) + } else { + self.addSubnode(self.leftShadowNode) + } + } + + self.typeIconNode.image = UIImage(bundleImageName: iconName) + if self.typeIconNode.supernode == nil { + self.addSubnode(self.typeIconNode) + } + self.setNeedsLayout() + } + } + override func layout() { super.layout() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 8097ef956a..e890be2a1f 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -33,7 +33,9 @@ import ComponentFlow import BundleIconComponent import LottieComponent import GlassBarButtonComponent +import GlassControls import AlertComponent +import MultilineTextComponent final class MediaPickerInteraction { let downloadManager: AssetDownloadManager @@ -237,6 +239,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let titleView: MediaPickerTitleView private let cancelButtonNode: WebAppCancelButtonNode + private var buttons: ComponentView? private var cancelButton: ComponentView? private var rightButton: ComponentView? private let moreButtonPlayOnce = ActionSlot() @@ -1164,7 +1167,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } - private func updateSelectionState(animated: Bool = false, updateLayout: Bool = true) { + fileprivate func updateSelectionState(animated: Bool = false, updateLayout: Bool = true) { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { itemNode.updateSelectionState(animated: animated) @@ -2102,6 +2105,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if case .glass = style { self.cancelButton = ComponentView() + self.buttons = ComponentView() } self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) @@ -2635,8 +2639,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att let useGlassButtons = (isBack || !self.controllerNode.scrolledToTop) && !self.controllerNode.isSwitchingAssetGroup let barButtonSideInset: CGFloat = 16.0 - let barButtonSize = CGSize(width: 44.0, height: 44.0) - + + var buttonTransition = ComponentTransition.easeInOut(duration: 0.25) if case let .animated(duration, _) = transition, duration > 0.25 { buttonTransition = .easeInOut(duration: duration) @@ -2659,124 +2663,248 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att transition.updateAlpha(layer: self.controllerNode.topEdgeEffectView.layer, alpha: self.controllerNode.scrolledExactlyToTop && self.controllerNode.currentDisplayMode == .all ? 0.0 : 1.0) self.controllerNode.topEdgeEffectView.update(content: topEdgeColor, blur: true, alpha: 0.8, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) - if let cancelButton = self.cancelButton { - if cancelButton.view == nil { - buttonTransition = .immediate - } - let cancelButtonSize = cancelButton.update( - transition: buttonTransition, - component: AnyComponent(GlassBarButtonComponent( - size: barButtonSize, - backgroundColor: nil, - isDark: self.presentationData.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( - BundleIconComponent( - name: isBack ? "Navigation/Back" : "Navigation/Close", - tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor - ) - )), - action: { [weak self] _ in - self?.cancelPressed() + let leftControlItems: [GlassControlGroupComponent.Item] = [ + GlassControlGroupComponent.Item( + id: AnyHashable(isBack ? "back" : "close"), + content: .icon(isBack ? "Navigation/Back" : "Navigation/Close"), + action: { [weak self] in + guard let self else { + return } - )), - environment: {}, - containerSize: barButtonSize - ) - let cancelButtonFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: cancelButtonSize) - if let view = cancelButton.view { - if view.superview == nil { - self.view.addSubview(view) + self.cancelPressed() + } + ) + ] + var rightControlItems: [GlassControlGroupComponent.Item] = [] + if moreIsVisible, case .glass = self.style { + if self.hasSelectButton { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("select"), + content: .text(self.presentationData.strings.Common_Select), + action: { [weak self] in + guard let self else { + return + } + self.selectPressed() + } + )) + } else { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("more"), + content: .animation("anim_morewide"), + action: { [weak self] in + guard let self else { + return + } + if let controlsView = self.buttons?.view as? GlassControlPanelComponent.View, let rightItemView = controlsView.rightItemView, let sourceView = rightItemView.itemView(id: AnyHashable("more")) { + self.searchOrMorePressed(view: sourceView, gesture: nil) + } + } + )) + } + + var hasLivePhoto = false + if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if let item = item as? TGMediaAsset, let asset = item.backingAsset { + if asset.mediaSubtypes.contains(.photoLive) { + hasLivePhoto = true + break + } + } + } + if hasLivePhoto { + let isForceLivePhoto = editingContext.isForceLivePhotoEnabled() + rightControlItems.insert(GlassControlGroupComponent.Item( + id: AnyHashable(isForceLivePhoto ? "livePhoto_on" : "livePhoto_off"), + content: .icon(isForceLivePhoto ? "Media Editor/LiveOn" : "Media Editor/LiveOff"), + action: { [weak self, weak editingContext] in + guard let self, let editingContext else { + return + } + editingContext.setForceLivePhotoEnabled(!isForceLivePhoto) + self.controllerNode.updateSelectionState() + + let iconName: String + let text: String + if !isForceLivePhoto { + iconName = "Media Editor/LiveLargeOn" + text = self.presentationData.strings.MediaPicker_LivePhoto_Tooltip_Live + } else { + iconName = "Media Editor/LiveLargeOff" + text = self.presentationData.strings.MediaPicker_LivePhoto_Tooltip_LiveOff + } + + let controller = TooltipInfoScreen( + context: self.context, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: iconName, tintColor: .white) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white))) + )) + ], spacing: 11.0)), + alignment: .bottom + ) + self.present(controller, in: .current) + } + ), at: 0) } - view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size) - view.center = cancelButtonFrame.center } } - if moreIsVisible, case .glass = self.style { - let moreButton: ComponentView - if let current = self.rightButton { - moreButton = current - } else { - moreButton = ComponentView() - self.rightButton = moreButton + if let buttons = self.buttons { + if buttons.view == nil { + buttonTransition = .immediate } - - let buttonComponent: AnyComponent - let rightButtonSize: CGSize - if self.hasSelectButton { - buttonComponent = AnyComponent(GlassBarButtonComponent( - size: nil, - backgroundColor: nil, - isDark: self.presentationData.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: "select", component: AnyComponent( - Text(text: self.presentationData.strings.Common_Select, font: Font.semibold(17.0), color: self.presentationData.theme.chat.inputPanel.panelControlColor) - )), - action: { [weak self] _ in - self?.selectPressed() - self?.moreButtonPlayOnce.invoke(Void()) - }) - ) - rightButtonSize = CGSize(width: 160.0, height: 44.0) - } else { - buttonComponent = AnyComponent(GlassBarButtonComponent( - size: barButtonSize, - backgroundColor: nil, - isDark: self.presentationData.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: "more", component: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent( - name: "anim_morewide" - ), - color: self.presentationData.theme.chat.inputPanel.panelControlColor, - size: CGSize(width: 34.0, height: 34.0), - playOnce: self.moreButtonPlayOnce - ) - )), - action: { [weak self] view in - self?.searchOrMorePressed(view: view, gesture: nil) - self?.moreButtonPlayOnce.invoke(Void()) - }) - ) - rightButtonSize = barButtonSize - } - - let moreButtonSize = moreButton.update( + let buttonsSize = buttons.update( transition: buttonTransition, - component: buttonComponent, + component: AnyComponent(GlassControlPanelComponent( + theme: self.presentationData.theme, + leftItem: GlassControlPanelComponent.Item( + items: leftControlItems, + background: .panel + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: self.presentationData.theme.overallDarkAppearance + )), environment: {}, - containerSize: rightButtonSize + containerSize: CGSize(width: layout.size.width - barButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0) ) - let moreButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - moreButtonSize.width - barButtonSideInset - layout.safeInsets.right, y: barButtonSideInset), size: moreButtonSize) - if let view = moreButton.view { + let buttonsFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: buttonsSize) + if let view = buttons.view { if view.superview == nil { self.view.addSubview(view) - if transition.isAnimated { - let transition = ComponentTransition(transition) - transition.animateAlpha(view: view, from: 0.0, to: 1.0) - transition.animateScale(view: view, from: 0.1, to: 1.0) - } - } - view.bounds = CGRect(origin: .zero, size: moreButtonFrame.size) - view.center = moreButtonFrame.center - } - } else if let moreButton = self.rightButton { - self.rightButton = nil - if let view = moreButton.view { - if transition.isAnimated { - let transition = ComponentTransition(transition) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - view.removeFromSuperview() - }) - transition.animateScale(view: view, from: 1.0, to: 0.1) - } else { - view.removeFromSuperview() } + view.bounds = CGRect(origin: .zero, size: buttonsFrame.size) + view.center = buttonsFrame.center } } + +// if let cancelButton = self.cancelButton { +// if cancelButton.view == nil { +// buttonTransition = .immediate +// } +// let cancelButtonSize = cancelButton.update( +// transition: buttonTransition, +// component: AnyComponent(GlassBarButtonComponent( +// size: barButtonSize, +// backgroundColor: nil, +// isDark: self.presentationData.theme.overallDarkAppearance, +// state: .glass, +// component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( +// BundleIconComponent( +// name: isBack ? "Navigation/Back" : "Navigation/Close", +// tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor +// ) +// )), +// action: { [weak self] _ in +// self?.cancelPressed() +// } +// )), +// environment: {}, +// containerSize: barButtonSize +// ) +// let cancelButtonFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: cancelButtonSize) +// if let view = cancelButton.view { +// if view.superview == nil { +// self.view.addSubview(view) +// } +// view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size) +// view.center = cancelButtonFrame.center +// } +// } +// +// if moreIsVisible, case .glass = self.style { +// let moreButton: ComponentView +// if let current = self.rightButton { +// moreButton = current +// } else { +// moreButton = ComponentView() +// self.rightButton = moreButton +// } +// +// let buttonComponent: AnyComponent +// let rightButtonSize: CGSize +// if self.hasSelectButton { +// buttonComponent = AnyComponent(GlassBarButtonComponent( +// size: nil, +// backgroundColor: nil, +// isDark: self.presentationData.theme.overallDarkAppearance, +// state: .glass, +// component: AnyComponentWithIdentity(id: "select", component: AnyComponent( +// Text(text: self.presentationData.strings.Common_Select, font: Font.semibold(17.0), color: self.presentationData.theme.chat.inputPanel.panelControlColor) +// )), +// action: { [weak self] _ in +// self?.selectPressed() +// }) +// ) +// rightButtonSize = CGSize(width: 160.0, height: 44.0) +// } else { +// buttonComponent = AnyComponent(GlassBarButtonComponent( +// size: barButtonSize, +// backgroundColor: nil, +// isDark: self.presentationData.theme.overallDarkAppearance, +// state: .glass, +// component: AnyComponentWithIdentity(id: "more", component: AnyComponent( +// LottieComponent( +// content: LottieComponent.AppBundleContent( +// name: "anim_morewide" +// ), +// color: self.presentationData.theme.chat.inputPanel.panelControlColor, +// size: CGSize(width: 34.0, height: 34.0), +// playOnce: self.moreButtonPlayOnce +// ) +// )), +// action: { [weak self] view in +// self?.searchOrMorePressed(view: view, gesture: nil) +// self?.moreButtonPlayOnce.invoke(Void()) +// }) +// ) +// rightButtonSize = barButtonSize +// } +// +// let moreButtonSize = moreButton.update( +// transition: buttonTransition, +// component: buttonComponent, +// environment: {}, +// containerSize: rightButtonSize +// ) +// let moreButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - moreButtonSize.width - barButtonSideInset - layout.safeInsets.right, y: barButtonSideInset), size: moreButtonSize) +// if let view = moreButton.view { +// if view.superview == nil { +// self.view.addSubview(view) +// if transition.isAnimated { +// let transition = ComponentTransition(transition) +// transition.animateAlpha(view: view, from: 0.0, to: 1.0) +// transition.animateScale(view: view, from: 0.1, to: 1.0) +// } +// } +// view.bounds = CGRect(origin: .zero, size: moreButtonFrame.size) +// view.center = moreButtonFrame.center +// } +// } else if let moreButton = self.rightButton { +// self.rightButton = nil +// if let view = moreButton.view { +// if transition.isAnimated { +// let transition = ComponentTransition(transition) +// view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in +// view.removeFromSuperview() +// }) +// transition.animateScale(view: view, from: 1.0, to: 0.1) +// } else { +// view.removeFromSuperview() +// } +// } +// } + if case .assets(_, .story) = self.subject { if self.selectionCount > 0 { let text = self.presentationData.strings.MediaPicker_CreateStory(self.selectionCount) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 7dc05dbdee..e19c5f723f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -128,6 +128,9 @@ extension TelegramUser { if (flags2 & (1 << 17)) != 0 { botFlags.insert(.forumManagedByUser) } + if (flags2 & (1 << 18)) != 0 { + botFlags.insert(.canManageBots) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index be2637e7d4..bcc02ef022 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -44,6 +44,7 @@ public struct BotUserInfoFlags: OptionSet { public static let hasWebApp = BotUserInfoFlags(rawValue: (1 << 7)) public static let hasForum = BotUserInfoFlags(rawValue: (1 << 8)) public static let forumManagedByUser = BotUserInfoFlags(rawValue: (1 << 9)) + public static let canManageBots = BotUserInfoFlags(rawValue: (1 << 10)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index 1412c0c9ea..a1a5efa82f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -388,6 +388,7 @@ func _internal_sendBotRequestedPeer(account: Account, peerId: PeerId, messageId: public enum CreateBotError { case generic case occupied + case limitExceeded } func _internal_createBot(account: Account, name: String, username: String, managerPeerId: PeerId, viaDeeplink: Bool) -> Signal { @@ -410,6 +411,8 @@ func _internal_createBot(account: Account, name: String, username: String, manag |> mapError { error -> CreateBotError in if error.errorDescription == "USERNAME_OCCUPIED" { return .occupied + } else if error.errorDescription == "BOT_CREATE_LIMIT_EXCEEDED" { + return .limitExceeded } else { return .generic } diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift index 998743a195..c52ee1aaae 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift @@ -317,6 +317,14 @@ public final class GlassControlPanelComponent: Component { return availableSize } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self { + return nil + } + return result + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/Contents.json index c8137a261e..7f50cdf533 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "livecorner.pdf", + "filename" : "livemini.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livemini.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livemini.pdf new file mode 100644 index 0000000000..4455fdce7b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livemini.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/Contents.json new file mode 100644 index 0000000000..68900d358d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "liveoffmini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/liveoffmini.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/liveoffmini.pdf new file mode 100644 index 0000000000..0ecb0ee562 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhotoOff.imageset/liveoffmini.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/Contents.json new file mode 100644 index 0000000000..9982135bb0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bounce (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livecorner.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/bounce (3).pdf similarity index 50% rename from submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livecorner.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/bounce (3).pdf index 51fc7f0ffb..6028cff0ee 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Message/LivePhoto.imageset/livecorner.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeBounce.imageset/bounce (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/Contents.json new file mode 100644 index 0000000000..76ace2ec60 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loop (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/loop (3).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/loop (3).pdf new file mode 100644 index 0000000000..a46d251874 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeLoop.imageset/loop (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/Contents.json new file mode 100644 index 0000000000..f791094a8e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "liveoff (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/liveoff (3).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/liveoff (3).pdf new file mode 100644 index 0000000000..0153ec2eaf Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOff.imageset/liveoff (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/Contents.json new file mode 100644 index 0000000000..70d6fd6afe --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "live (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/live (3).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/live (3).pdf new file mode 100644 index 0000000000..2c4297795d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/LiveLargeOn.imageset/live (3).pdf differ diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index e739b869fe..30ecc50713 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -9,6 +9,7 @@ import AccountContext import ShareController import UndoUI import AttachmentFileController +import LegacyMediaPickerUI final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayerController { private let context: AccountContext @@ -156,9 +157,24 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer guard let self, let navigationController = self.parentNavigationController else { return } + var dismissImpl: (() -> Void)? let controller = makeAttachmentFileControllerImpl( context: self.context, mode: .audio(.savedMusic), + presentFiles: { [weak self] in + guard let self else { + return + } + dismissImpl?() + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .import, documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"], completion: { urls in + guard let url = urls.first else { + return + } + print(url) + }) + self.present(controller, in: .window(.root)) + }, send: { [weak self] mediaReferences, _, _, _ in guard let self, let reference = mediaReferences.first?.concrete(TelegramMediaFile.self) else { return @@ -168,6 +184,9 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer ) as! AttachmentFileControllerImpl controller.navigationPresentation = .modal navigationController.pushViewController(controller) + dismissImpl = { [weak controller] in + controller?.dismiss() + } }, getParentController: { [weak self] in return self diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index a545b7ed3e..6210ba72fb 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -871,14 +871,14 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu ) ) } - rightControlItems.append( - GlassControlGroupComponent.Item( - id: AnyHashable("search"), - content: .icon("Navigation/Search"), - action: { - } - ) - ) +// rightControlItems.append( +// GlassControlGroupComponent.Item( +// id: AnyHashable("search"), +// content: .icon("Navigation/Search"), +// action: { +// } +// ) +// ) let headerInset: CGFloat = 16.0 let headerButtonsSize = self.headerButtons.update( @@ -891,7 +891,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu background: .panel ), centralItem: nil, - rightItem: GlassControlPanelComponent.Item( + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( items: rightControlItems, background: .panel ),