11import Foundation
22import UIKit
33import ComponentFlow
4+ import Display
5+ import AppBundle
46import AccountContext
57import TelegramCore
68import InstantPageUI
79import RichTextEditorUIKit
810import MosaicLayout
11+ import GlassBackgroundComponent
912
1013/// Adapts the rich-text editor's `RichTextMediaItemView` seam to concrete media renderers, by kind:
1114/// audio (music/voice `.file`) → `StandaloneInstantPageAudioView` (a playable row); location (`.geo`)
@@ -32,6 +35,18 @@ public final class MediaItemNodeView: UIView, RichTextMediaItemView {
3235 private let mosaicContext : AccountContext ? // set for photo/video; nil for audio/location
3336 private let showsControls : Bool // forwarded to each photo/video cell's glass "more" button
3437
38+ // A dedicated "+" add button (article editor, photo/video containers only). Glass chrome mirroring the
39+ // per-cell ⋯ button (`RichTextMediaContentComponent.View`'s `moreButton`/`moreButtonBackgroundContainer`/
40+ // `moreButtonBackground`); top-right corner. Fires `.add` (container-level, itemIndex nil).
41+ private var addButtonContainer : GlassBackgroundContainerView ?
42+ private var addButton : HighlightTrackingButton ?
43+ private var addButtonBackground : GlassBackgroundView ?
44+ private var addButtonIconView : UIImageView ?
45+
46+ /// True when this view should show the add button: article editor (`showsControls`) + a photo/video
47+ /// container (`mosaicContext != nil` — never audio/location).
48+ private var showsAddButton : Bool { self . showsControls && self . mosaicContext != nil }
49+
3550 public init ( context: AccountContext ,
3651 items: [ ( media: EngineMedia , naturalSize: CGSize ) ] ,
3752 audioColorOverride: InstantPageAudioColorOverride ? = nil ,
@@ -89,6 +104,62 @@ public final class MediaItemNodeView: UIView, RichTextMediaItemView {
89104 if self . mosaicContext != nil {
90105 self . updateMosaic ( size: size)
91106 }
107+ self . layoutAddButton ( size: size)
108+ }
109+
110+ /// Builds the add button's glass chrome on first use (article-editor photo/video containers only).
111+ /// Mirrors `RichTextMediaContentComponent.View`'s more-button setup exactly: background inside the
112+ /// button, button inside the container's `contentView`, container added last (above everything else).
113+ private func ensureAddButton( ) {
114+ guard self . addButtonContainer == nil else { return }
115+ let container = GlassBackgroundContainerView ( )
116+ let background = GlassBackgroundView ( )
117+ let button = HighlightTrackingButton ( )
118+ background. isUserInteractionEnabled = false
119+ button. addSubview ( background)
120+ container. contentView. addSubview ( button)
121+ self . addSubview ( container)
122+ let iconView = UIImageView ( image: generateTintedImage ( image: UIImage ( bundleImageName: " Chat/Context Menu/Add " ) , color: . white) )
123+ iconView. contentMode = . center
124+ button. addSubview ( iconView)
125+ button. addTarget ( self , action: #selector( self . addButtonPressed) , for: . touchUpInside)
126+ button. highligthedChanged = { [ weak button] highlighted in
127+ guard let button else { return }
128+ let transition : ComponentTransition = highlighted ? . immediate : . easeInOut( duration: 0.25 )
129+ transition. setAlpha ( view: button, alpha: highlighted ? 0.6 : 1.0 )
130+ }
131+ self . addButtonContainer = container
132+ self . addButton = button
133+ self . addButtonBackground = background
134+ self . addButtonIconView = iconView
135+ }
136+
137+ @objc private func addButtonPressed( ) {
138+ guard let container = self . addButtonContainer else { return }
139+ self . onControlTapped ? ( . add, nil , container, container. bounds)
140+ }
141+
142+ /// Positions the add button top-right and shows/hides it (`showsAddButton`). Brought above the mosaic
143+ /// cells every pass since cell hosts are inserted after it may have been created.
144+ private func layoutAddButton( size: CGSize ) {
145+ guard self . showsAddButton else {
146+ self . addButtonContainer? . isHidden = true
147+ return
148+ }
149+ self . ensureAddButton ( )
150+ guard let container = self . addButtonContainer, let button = self . addButton,
151+ let background = self . addButtonBackground, let iconView = self . addButtonIconView else { return }
152+ container. isHidden = false
153+ let buttonSize = CGSize ( width: 36.0 , height: 36.0 )
154+ let inset : CGFloat = 8.0
155+ let frame = CGRect ( x: size. width - inset - buttonSize. width, y: inset, width: buttonSize. width, height: buttonSize. height) // top-right
156+ container. frame = frame
157+ container. update ( size: buttonSize, isDark: true , transition: . immediate)
158+ background. frame = CGRect ( origin: . zero, size: buttonSize)
159+ background. update ( size: buttonSize, cornerRadius: buttonSize. height * 0.5 , isDark: true , tintColor: . init( kind: . panel) , transition: . immediate)
160+ button. frame = CGRect ( origin: . zero, size: buttonSize)
161+ iconView. frame = CGRect ( origin: . zero, size: buttonSize)
162+ self . bringSubviewToFront ( container) // above the mosaic cells
92163 }
93164
94165 /// Lays out photo/video cells, reusing an existing cell wherever `MosaicCellDiff` finds a matching pooled
@@ -170,18 +241,27 @@ public final class MediaItemNodeView: UIView, RichTextMediaItemView {
170241 if self . mosaicContext != nil {
171242 self . updateMosaic ( size: self . bounds. size)
172243 }
244+ self . layoutAddButton ( size: self . bounds. size)
173245 }
174246
175247 /// The editor treats media as non-interactive EXCEPT for the interactive controls the photo/video
176- /// renderer exposes (the more button). Only a hit that resolves to such a control claims the touch;
177- /// everything else — the poster area, and the audio/location branches entirely — returns nil so the
178- /// touch falls through to the editor's own tap handling. Guarded by the standard visibility/point-inside
179- /// checks so a culled (hidden) media view never claims a touch.
248+ /// renderer exposes (the per-cell more button, and — article editor only — this container's own add
249+ /// button). Only a hit that resolves to such a control claims the touch; everything else — the poster
250+ /// area, and the audio/location branches entirely — returns nil so the touch falls through to the
251+ /// editor's own tap handling. Guarded by the standard visibility/point-inside checks so a culled
252+ /// (hidden) media view never claims a touch. The add button is checked FIRST since it visually sits
253+ /// above the mosaic cells in the top-right corner.
180254 override public func hitTest( _ point: CGPoint , with event: UIEvent ? ) -> UIView ? {
181255 guard !self . isHidden, self . isUserInteractionEnabled, self . alpha > 0.01 ,
182256 self . point ( inside: point, with: event) else {
183257 return nil
184258 }
259+ if self . showsAddButton, let container = self . addButtonContainer, !container. isHidden {
260+ let inContainer = container. convert ( point, from: self )
261+ if let hit = container. hitTest ( inContainer, with: event) {
262+ return hit
263+ }
264+ }
185265 if self . mosaicContext != nil {
186266 for (_, cell) in self . mosaicCells {
187267 let inHost = cell. host. convert ( point, from: self )
0 commit comments