Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/one-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
}
},
"scripts": {
"build": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
"watch": "npm run build -- --watch",
"build": "sh -c 'esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node'",
"watch": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --watch",
"package": "vsce package"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions packages/one/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["apple"],
"apple": {
"modules": ["OneLinkPreviewModule"]
}
}
195 changes: 195 additions & 0 deletions packages/one/ios/LinkPreview/LinkPreviewNativeActionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import ExpoModulesCore

class LinkPreviewNativeActionView: RouterViewWithLogger, LinkPreviewMenuUpdatable {
var identifier: String = ""
// MARK: - Shared props
@NativeActionProp(updateAction: true, updateMenu: true) var title: String = ""
@NativeActionProp(updateAction: true, updateMenu: true) var icon: String?
@NativeActionProp(updateAction: true, updateMenu: true) var destructive: Bool?
@NativeActionProp(updateAction: true, updateMenu: true) var disabled: Bool = false

// MARK: - Action only props
@NativeActionProp(updateAction: true) var isOn: Bool?
@NativeActionProp(updateAction: true) var keepPresented: Bool?
@NativeActionProp(updateAction: true) var discoverabilityLabel: String?
@NativeActionProp(updateAction: true, updateMenu: true) var subtitle: String?

// MARK: - Menu only props
@NativeActionProp(updateMenu: true) var singleSelection: Bool = false
@NativeActionProp(updateMenu: true) var displayAsPalette: Bool = false
@NativeActionProp(updateMenu: true) var displayInline: Bool = false
@NativeActionProp(updateMenu: true) var preferredElementSize: MenuElementSize?

// MARK: - UIBarButtonItem props
@NativeActionProp(updateAction: true, updateMenu: true) var routerHidden: Bool = false
@NativeActionProp(updateMenu: true) var titleStyle: TitleStyle?
@NativeActionProp(updateMenu: true) var sharesBackground: Bool?
@NativeActionProp(updateMenu: true) var hidesSharedBackground: Bool?
@NativeActionProp(updateAction: true, updateMenu: true) var customTintColor: UIColor?
@NativeActionProp(updateMenu: true) var barButtonItemStyle: UIBarButtonItem.Style?
@NativeActionProp(updateMenu: true) var subActions: [LinkPreviewNativeActionView] = []
@NativeActionProp(updateMenu: true) var accessibilityLabelForMenu: String?
@NativeActionProp(updateMenu: true) var accessibilityHintForMenu: String?

// MARK: - Events
let onSelected = EventDispatcher()

// MARK: - Native API
weak var parentMenuUpdatable: LinkPreviewMenuUpdatable?

private var baseUiAction: UIAction
private var menuAction: UIMenu

var isMenuAction: Bool {
return !subActions.isEmpty
}

var uiAction: UIMenuElement {
isMenuAction ? menuAction : baseUiAction
}

required init(appContext: AppContext? = nil) {
baseUiAction = UIAction(title: "", handler: { _ in })
menuAction = UIMenu(title: "", image: nil, options: [], children: [])
super.init(appContext: appContext)
clipsToBounds = true
baseUiAction = UIAction(title: "", handler: { _ in self.onSelected() })
}

func updateMenu() {
let subActions = subActions.map { subAction in
subAction.uiAction
}
var options: UIMenu.Options = []
if #available(iOS 17.0, *) {
if displayAsPalette {
options.insert(.displayAsPalette)
}
}
if singleSelection {
options.insert(.singleSelection)
}
if displayInline {
options.insert(.displayInline)
}
if destructive == true {
options.insert(.destructive)
}

menuAction = UIMenu(
title: title,
image: icon.flatMap { UIImage(systemName: $0) },
options: options,
children: subActions
)

if let subtitle = subtitle {
menuAction.subtitle = subtitle
}

if #available(iOS 16.0, *) {
if let preferredElementSize = preferredElementSize {
menuAction.preferredElementSize = preferredElementSize.toUIMenuElementSize()
}
}

parentMenuUpdatable?.updateMenu()
}

func updateUiAction() {
var attributes: UIMenuElement.Attributes = []
if destructive == true { attributes.insert(.destructive) }
if disabled == true { attributes.insert(.disabled) }
if routerHidden {
attributes.insert(.hidden)
}

if #available(iOS 16.0, *) {
if keepPresented == true { attributes.insert(.keepsMenuPresented) }
}

baseUiAction.title = title
baseUiAction.image = icon.flatMap { UIImage(systemName: $0) }
baseUiAction.attributes = attributes
baseUiAction.state = isOn == true ? .on : .off

if let subtitle = subtitle {
baseUiAction.subtitle = subtitle
}
if let label = discoverabilityLabel {
baseUiAction.discoverabilityTitle = label
}

parentMenuUpdatable?.updateMenu()
}

override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
if let childActionView = childComponentView as? LinkPreviewNativeActionView {
subActions.insert(childActionView, at: index)
childActionView.parentMenuUpdatable = self
} else {
logger?.warn(
"[one-router] Unknown child component view (\(childComponentView)) mounted to NativeLinkPreviewActionView. This is most likely a bug in one-router."
)
}
}

override func unmountChildComponentView(_ child: UIView, index: Int) {
if let childActionView = child as? LinkPreviewNativeActionView {
subActions.removeAll(where: { $0 == childActionView })
} else {
logger?.warn(
"[one-router] Unknown child component view (\(child)) unmounted from NativeLinkPreviewActionView. This is most likely a bug in one-router."
)
}
}

@propertyWrapper
struct NativeActionProp<Value: Equatable> {
var value: Value
let updateAction: Bool
let updateMenu: Bool

init(wrappedValue: Value, updateAction: Bool = false, updateMenu: Bool = false) {
self.value = wrappedValue
self.updateAction = updateAction
self.updateMenu = updateMenu
}

static subscript<EnclosingSelf: LinkPreviewNativeActionView>(
_enclosingInstance instance: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, NativeActionProp<Value>>
) -> Value {
get {
instance[keyPath: storageKeyPath].value
}
set {
let oldValue = instance[keyPath: storageKeyPath].value
if oldValue != newValue {
instance[keyPath: storageKeyPath].value = newValue
if instance[keyPath: storageKeyPath].updateAction {
instance.updateUiAction()
}
if instance[keyPath: storageKeyPath].updateMenu {
instance.updateMenu()
}
}
}
}

var wrappedValue: Value {
get { value }
set { value = newValue }
}
}
}

// Needed to allow optional properties without default `= nil` to avoid repetition
extension LinkPreviewNativeActionView.NativeActionProp where Value: ExpressibleByNilLiteral {
init(updateAction: Bool = false, updateMenu: Bool = false) {
self.value = nil
self.updateAction = updateAction
self.updateMenu = updateMenu
}
}
Loading