Files
bsky-social-app/modules/expo-bluesky-context-menu/ios/MenuBuilder.swift
T
Claude d4fd15dc20 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
2026-05-25 14:35:14 +03:00

52 lines
1.7 KiB
Swift

import UIKit
/// Builds a `UIMenu` from the JS-shipped item specs. Each item may carry an
/// icon spec (SVG path data) which is rasterized via `IconRenderer`.
enum MenuBuilder {
/// Expected item shape from JS:
/// {
/// id: String,
/// label: String,
/// destructive?: Bool,
/// disabled?: Bool,
/// icon?: {
/// paths: [String],
/// viewBox: String,
/// strokeWidth: Double
/// }
/// }
static func build(items: [[String: Any]], onSelect: @escaping (String) -> Void) -> UIMenu {
let actions: [UIMenuElement] = items.compactMap { spec in
guard let id = spec["id"] as? String,
let label = spec["label"] as? String else { return nil }
let destructive = (spec["destructive"] as? Bool) ?? false
let disabled = (spec["disabled"] as? Bool) ?? false
let image = icon(from: spec["icon"] as? [String: Any])
var attributes: UIMenuElement.Attributes = []
if destructive { attributes.insert(.destructive) }
if disabled { attributes.insert(.disabled) }
return UIAction(title: label, image: image, attributes: attributes) { _ in
onSelect(id)
}
}
return UIMenu(title: "", children: actions)
}
private static func icon(from spec: [String: Any]?) -> UIImage? {
guard let spec = spec,
let paths = spec["paths"] as? [String], !paths.isEmpty else { return nil }
let viewBox = (spec["viewBox"] as? String) ?? "0 0 24 24"
let strokeWidth = CGFloat((spec["strokeWidth"] as? Double) ?? 0)
let renderSpec = IconRenderer.Spec(
paths: paths,
viewBox: viewBox,
strokeWidth: strokeWidth,
pointSize: 24
)
return IconRenderer.image(for: renderSpec)
}
}