Add iOS peek long-press context menu for image embeds
Introduces expo-bluesky-context-menu, a compositional Root/Trigger/Menu/MenuItem wrapper around UIContextMenuInteraction. The preview ViewController is sized to the image's true aspect ratio so tall/panorama previews don't stretch mid-lift, and menu icons are rasterized from the app's SVG icon set on the native side. The preview prop is a discriminated union (image today; video and externalCard reserved) so the same module can back those embeds in follow-ups. https://claude.ai/code/session_015REmux3R9uuEMMJUHxTyQT
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
|
||||
/// Native view that hosts the children and attaches a
|
||||
/// `UIContextMenuInteraction`. Two JS-shipped props drive behaviour:
|
||||
/// - `preview`: discriminated union describing what to show during peek
|
||||
/// - `menuItems`: array of menu item specs (see `MenuBuilder`)
|
||||
/// - `borderRadius`: used for the targeted preview's visible path so the lift
|
||||
/// animation matches the thumbnail's clipping
|
||||
class ExpoBlueskyContextMenuView: ExpoView, UIContextMenuInteractionDelegate {
|
||||
private var preview: [String: Any]?
|
||||
private var menuItems: [[String: Any]] = []
|
||||
private var borderRadius: CGFloat = 0
|
||||
|
||||
private let onItemPress = EventDispatcher()
|
||||
private let onPreviewPress = EventDispatcher()
|
||||
|
||||
private var pendingCommitId: String?
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
let interaction = UIContextMenuInteraction(delegate: self)
|
||||
self.addInteraction(interaction)
|
||||
}
|
||||
|
||||
func setPreview(_ value: [String: Any]?) { self.preview = value }
|
||||
func setMenuItems(_ value: [[String: Any]]) { self.menuItems = value }
|
||||
func setBorderRadius(_ value: Double) { self.borderRadius = CGFloat(value) }
|
||||
|
||||
// MARK: - UIContextMenuInteractionDelegate
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
configurationForMenuAtLocation location: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
let previewSpec = self.preview
|
||||
let items = self.menuItems
|
||||
|
||||
return UIContextMenuConfiguration(
|
||||
identifier: nil,
|
||||
previewProvider: { [weak self] in
|
||||
guard self != nil else { return nil }
|
||||
return PreviewFactory.makeController(from: previewSpec)
|
||||
},
|
||||
actionProvider: { [weak self] _ in
|
||||
guard let self = self else { return nil }
|
||||
return MenuBuilder.build(items: items) { [weak self] id in
|
||||
self?.onItemPress(["id": id])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
|
||||
) -> UITargetedPreview? {
|
||||
return makeTargetedPreview()
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration
|
||||
) -> UITargetedPreview? {
|
||||
return makeTargetedPreview()
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
|
||||
animator: UIContextMenuInteractionCommitAnimating
|
||||
) {
|
||||
animator.addCompletion { [weak self] in
|
||||
self?.onPreviewPress([:])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Targeted preview
|
||||
|
||||
/// The targeted preview uses the view itself as target with a rounded-corner
|
||||
/// visible path matching the thumbnail's clipping, so the lift animation
|
||||
/// respects the existing corner radius.
|
||||
private func makeTargetedPreview() -> UITargetedPreview {
|
||||
let parameters = UIPreviewParameters()
|
||||
parameters.backgroundColor = .clear
|
||||
if borderRadius > 0 {
|
||||
parameters.visiblePath = UIBezierPath(
|
||||
roundedRect: self.bounds,
|
||||
cornerRadius: borderRadius
|
||||
)
|
||||
}
|
||||
return UITargetedPreview(view: self, parameters: parameters)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user