├── .gitattributes ├── .gitignore ├── .npmignore ├── README.md ├── android ├── README.md ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── reactlibrary │ ├── MediaClipboardModule.java │ └── MediaClipboardPackage.java ├── index.js ├── ios ├── MediaClipboard-Bridging-Header.h ├── MediaClipboard.h ├── MediaClipboard.swift ├── MediaClipboard.xcodeproj │ └── project.pbxproj ├── MediaClipboard.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MediaClipboardC++Interop.h ├── MediaClipboardC++Interop.mm ├── MediaClipboardJSI.h ├── MediaClipboardJSI.mm ├── MimeType.swift ├── YeetJSIUtils.h └── YeetJSIUtils.mm ├── package.json ├── react-native-media-clipboard.podspec ├── scripts └── examples_postinstall.js ├── src ├── MediaClipboard.ts ├── MediaClipboardContext.tsx └── index.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | 11 | dist 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | # Android/IntelliJ 34 | # 35 | build/ 36 | .idea 37 | .gradle 38 | local.properties 39 | *.iml 40 | 41 | # BUCK 42 | buck-out/ 43 | \.buckd/ 44 | *.keystore 45 | 46 | ios/Build 47 | ios/DerivedData 48 | ios/Pods 49 | lib/ 50 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | node_modules 3 | ios/Pods 4 | ios/Build 5 | ios/DerivedData 6 | ios/Pods -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-media-clipboard 2 | 3 | 4 | React Native has several libraries that let you get the contents of the clipboard, but none of them support images. 5 | 6 | `react-native-media-clipboard` suports: 7 | 8 | - images 9 | - strings 10 | - URLs 11 | 12 | ## Getting started 13 | 14 | `$ npm install react-native-media-clipboard --save` 15 | 16 | ### Installation (iOS only) 17 | 18 | 1. `cd ios && pod install` 19 | 2. Add the following line near the top of `AppDelegate.h`: 20 | 21 | ```h 22 | #import 23 | ``` 24 | 25 | 3. [Optional] Inside the AppDelegate `@implementation` add this: 26 | 27 | ```objc 28 | - (void)applicationDidBecomeActive:(UIApplication *)application { 29 | [MediaClipboard onApplicationBecomeActive]; 30 | } 31 | ``` 32 | 33 | This makes sure that the clipboard is in sync if the application went into the background. 34 | 35 | ##### Swift bridging header 36 | 37 | If your project does not contain any Swift code, then you need to create a bridging header – or you'll get a bunch of strange build errors. 38 | 39 | 4. Xcode -> File -> New -> Create an empty .swift file. It will prompt you asking if you want to create a bridging header. Say yes. 40 | 41 | If your project already has Swift code (or a bridging header), just ignore this step. 42 | 43 | 5. Re-build your app: `react-native run-ios` 44 | 45 | ## Usage 46 | 47 | ```javascript 48 | import { 49 | ClipboardContext, 50 | ClipboardProvider 51 | } from "react-native-media-clipboard"; 52 | ``` 53 | 54 | 7. At the root of your application, add `` in the render method, like this: 55 | 56 | ```javascript 57 | 58 | {children} 59 | 60 | ``` 61 | 62 | 8. `ClipboardContext` contains a `clipboard` and a `mediaSource` object. It automatically updates whenever the user copies something to their clipboard or removes something from their clipboard. 63 | 64 | ```javascript 65 | const { clipboard, mediaSource } = React.useContext(ClipboardContext); 66 | 67 | // Example mediaSource: 68 | { 69 | "mimeType": "image/png", 70 | "scale": 1, 71 | "width": 828, 72 | "uri": "file:///tmp/C4A65610-E644-44C2-AC54-25A8AD56A4C6.png", 73 | "height": 1792 74 | } 75 | 76 | // Example clipboard: 77 | { 78 | urls: [], 79 | strings: [], 80 | hasImages: false, 81 | hasURLs: false, 82 | hasStrings: false 83 | }; 84 | 85 | // You can just pass in the `mediaSource` object to the built-in Image component. As long as the mediaSource object is not null, it should just work. 86 | 87 | ``` 88 | 89 | There are type definitions for these, so you shouldn't need to refer back to this much. 90 | 91 | --- 92 | 93 | This library is iOS only. There is no Android support. 94 | 95 | Images are saved in the temporary directory for the app in a background thread. It does not send `data` URIs across the bridge. 96 | 97 | There is a JSI implementation of this as well, however I haven't finished porting it to this library. A contributor is welcome to submit a PR for that :) 98 | 99 | ### Example repo 100 | 101 | Example repo: [react-native-media-clipboard-example](https://github.com/Jarred-Sumner/react-native-media-clipboard-example) 102 | 103 | 104 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm: 5 | 6 | 1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed 7 | 2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK 8 | ``` 9 | ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle 10 | sdk.dir=/Users/{username}/Library/Android/sdk 11 | ``` 12 | 3. Delete the `maven` folder 13 | 4. Run `./gradlew installArchives` 14 | 5. Verify that latest set of generated files is in the maven folder with the correct version number 15 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // android/build.gradle 2 | 3 | // based on: 4 | // 5 | // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle 6 | // original location: 7 | // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle 8 | // 9 | // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle 10 | // original location: 11 | // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle 12 | 13 | def DEFAULT_COMPILE_SDK_VERSION = 28 14 | def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3' 15 | def DEFAULT_MIN_SDK_VERSION = 16 16 | def DEFAULT_TARGET_SDK_VERSION = 28 17 | 18 | def safeExtGet(prop, fallback) { 19 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | apply plugin: 'maven' 24 | 25 | buildscript { 26 | // The Android Gradle plugin is only required when opening the android folder stand-alone. 27 | // This avoids unnecessary downloads and potential conflicts when the library is included as a 28 | // module dependency in an application project. 29 | // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies 30 | if (project == rootProject) { 31 | repositories { 32 | google() 33 | jcenter() 34 | } 35 | dependencies { 36 | classpath 'com.android.tools.build:gradle:3.4.1' 37 | } 38 | } 39 | } 40 | 41 | apply plugin: 'com.android.library' 42 | apply plugin: 'maven' 43 | 44 | android { 45 | compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) 46 | buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) 47 | defaultConfig { 48 | minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) 49 | targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) 50 | versionCode 1 51 | versionName "1.0" 52 | } 53 | lintOptions { 54 | abortOnError false 55 | } 56 | } 57 | 58 | repositories { 59 | // ref: https://www.baeldung.com/maven-local-repository 60 | mavenLocal() 61 | maven { 62 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 63 | url "$rootDir/../node_modules/react-native/android" 64 | } 65 | maven { 66 | // Android JSC is installed from npm 67 | url "$rootDir/../node_modules/jsc-android/dist" 68 | } 69 | google() 70 | jcenter() 71 | } 72 | 73 | dependencies { 74 | //noinspection GradleDynamicVersion 75 | implementation 'com.facebook.react:react-native:+' // From node_modules 76 | } 77 | 78 | def configureReactNativePom(def pom) { 79 | def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text) 80 | 81 | pom.project { 82 | name packageJson.title 83 | artifactId packageJson.name 84 | version = packageJson.version 85 | group = "com.reactlibrary" 86 | description packageJson.description 87 | url packageJson.repository.baseUrl 88 | 89 | licenses { 90 | license { 91 | name packageJson.license 92 | url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename 93 | distribution 'repo' 94 | } 95 | } 96 | 97 | developers { 98 | developer { 99 | id packageJson.author.username 100 | name packageJson.author.name 101 | } 102 | } 103 | } 104 | } 105 | 106 | afterEvaluate { project -> 107 | // some Gradle build hooks ref: 108 | // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html 109 | task androidJavadoc(type: Javadoc) { 110 | source = android.sourceSets.main.java.srcDirs 111 | classpath += files(android.bootClasspath) 112 | classpath += files(project.getConfigurations().getByName('compile').asList()) 113 | include '**/*.java' 114 | } 115 | 116 | task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { 117 | classifier = 'javadoc' 118 | from androidJavadoc.destinationDir 119 | } 120 | 121 | task androidSourcesJar(type: Jar) { 122 | classifier = 'sources' 123 | from android.sourceSets.main.java.srcDirs 124 | include '**/*.java' 125 | } 126 | 127 | android.libraryVariants.all { variant -> 128 | def name = variant.name.capitalize() 129 | def javaCompileTask = variant.javaCompileProvider.get() 130 | 131 | task "jar${name}"(type: Jar, dependsOn: javaCompileTask) { 132 | from javaCompileTask.destinationDir 133 | } 134 | } 135 | 136 | artifacts { 137 | archives androidSourcesJar 138 | archives androidJavadocJar 139 | } 140 | 141 | task installArchives(type: Upload) { 142 | configuration = configurations.archives 143 | repositories.mavenDeployer { 144 | // Deploy to react-native-event-bridge/maven, ready to publish to npm 145 | repository url: "file://${projectDir}/../android/maven" 146 | configureReactNativePom pom 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/MediaClipboardModule.java: -------------------------------------------------------------------------------- 1 | package com.reactlibrary; 2 | 3 | import com.facebook.react.bridge.ReactApplicationContext; 4 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 5 | import com.facebook.react.bridge.ReactMethod; 6 | import com.facebook.react.bridge.Callback; 7 | 8 | public class MediaClipboardModule extends ReactContextBaseJavaModule { 9 | 10 | private final ReactApplicationContext reactContext; 11 | 12 | public MediaClipboardModule(ReactApplicationContext reactContext) { 13 | super(reactContext); 14 | this.reactContext = reactContext; 15 | } 16 | 17 | @Override 18 | public String getName() { 19 | return "MediaClipboard"; 20 | } 21 | 22 | @ReactMethod 23 | public void sampleMethod(String stringArgument, int numberArgument, Callback callback) { 24 | // TODO: Implement some actually useful functionality 25 | callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/MediaClipboardPackage.java: -------------------------------------------------------------------------------- 1 | package com.reactlibrary; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.bridge.NativeModule; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.uimanager.ViewManager; 11 | import com.facebook.react.bridge.JavaScriptModule; 12 | 13 | public class MediaClipboardPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new MediaClipboardModule(reactContext)); 17 | } 18 | 19 | @Override 20 | public List createViewManagers(ReactApplicationContext reactContext) { 21 | return Collections.emptyList(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * from "./src/index.ts"; 2 | -------------------------------------------------------------------------------- /ios/MediaClipboard-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | #import 8 | #import 9 | 10 | @interface RCT_EXTERN_MODULE(MediaClipboard, RCTEventEmitter) 11 | 12 | RCT_EXTERN_METHOD(getContent:(RCTResponseSenderBlock)callback); 13 | RCT_EXTERN_METHOD(clipboardMediaSource:(RCTResponseSenderBlock)callback); 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/MediaClipboard.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaClipboard.h 3 | // MediaClipboard 4 | // 5 | // Created by Jarred WSumner on 2/14/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | #import "MediaClipboard-Bridging-Header.h" 10 | 11 | @interface MediaClipboard (ext) 12 | + (void)onApplicationBecomeActive; 13 | @end 14 | -------------------------------------------------------------------------------- /ios/MediaClipboard.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // MediaClipboard.swift 5 | // Media 6 | // 7 | // Created by Jarred WSumner on 12/13/19. 8 | // Copyright © 2019 Yeet. All rights reserved. 9 | // 10 | 11 | import Foundation 12 | import UIKit 13 | 14 | @objc(MediaClipboard) 15 | class MediaClipboard: RCTEventEmitter { 16 | static let clipboardOperationQueue: OperationQueue = { 17 | var queue = OperationQueue() 18 | queue.name = "MediaClipboard" 19 | queue.maxConcurrentOperationCount = 1 20 | 21 | return queue 22 | }() 23 | 24 | enum EventNames : String { 25 | case change = "MediaClipboardChange" 26 | case remove = "MediaClipboardRemove" 27 | } 28 | 29 | static var changeCount = UIPasteboard.general.changeCount 30 | var listenerCount = 0 31 | 32 | @objc (onApplicationBecomeActive) static func onApplicationBecomeActive() { 33 | if changeCount != UIPasteboard.general.changeCount { 34 | NotificationCenter.default.post(name: UIPasteboard.changedNotification, object: nil) 35 | changeCount = UIPasteboard.general.changeCount 36 | } 37 | } 38 | 39 | override func startObserving() { 40 | super.startObserving() 41 | 42 | let needsSubscription = !hasListeners 43 | listenerCount += 1 44 | 45 | if needsSubscription { 46 | self.observePasteboardChange() 47 | } 48 | 49 | } 50 | 51 | override func stopObserving() { 52 | super.stopObserving() 53 | listenerCount -= 1 54 | 55 | let needsUnsubscription = !hasListeners 56 | 57 | if needsUnsubscription { 58 | self.stopObservingPasteboardChange() 59 | } 60 | } 61 | 62 | func observePasteboardChange() { 63 | NotificationCenter.default.addObserver(self, selector: #selector(handleChangeEvent(_:)), name: UIPasteboard.changedNotification, object: nil) 64 | NotificationCenter.default.addObserver(self, selector: #selector(handleRemoveEvent(_:)), name: UIPasteboard.removedNotification, object: nil) 65 | } 66 | 67 | func stopObservingPasteboardChange() { 68 | NotificationCenter.default.removeObserver(self, name: UIPasteboard.changedNotification, object: nil) 69 | NotificationCenter.default.removeObserver(self, name: UIPasteboard.removedNotification, object: nil) 70 | } 71 | 72 | @objc func handleChangeEvent(_ notification: NSNotification) { 73 | lastDictionaryValue = nil 74 | self.sendChangeEvent() 75 | MediaClipboard.changeCount = UIPasteboard.general.changeCount 76 | } 77 | 78 | @objc func handleRemoveEvent(_ notification: NSNotification) { 79 | lastDictionaryValue = nil 80 | self.sendChangeEvent() 81 | MediaClipboard.changeCount = UIPasteboard.general.changeCount 82 | } 83 | 84 | var hasListeners: Bool { 85 | return listenerCount > 0 86 | } 87 | 88 | override init() { 89 | super.init() 90 | } 91 | 92 | override var bridge: RCTBridge! { 93 | get { 94 | return super.bridge 95 | } 96 | 97 | set (newValue) { 98 | super.bridge = newValue 99 | 100 | 101 | } 102 | } 103 | 104 | 105 | func sendChangeEvent() { 106 | guard hasListeners else { 107 | return 108 | } 109 | 110 | sendEvent(withName: EventNames.change.rawValue, body: MediaClipboard.serializeContents()) 111 | } 112 | 113 | func sendRemoveEvent() { 114 | guard hasListeners else { 115 | return 116 | } 117 | 118 | sendEvent(withName: EventNames.remove.rawValue, body: MediaClipboard.serializeContents()) 119 | } 120 | 121 | override static func moduleName() -> String! { 122 | return "MediaClipboard"; 123 | } 124 | 125 | @objc(serializeContents) 126 | static func serializeContents() -> [String: Any] { 127 | var hasURLs = false 128 | var hasStrings = false 129 | 130 | 131 | if #available(iOS 10.0, *) { 132 | hasStrings = UIPasteboard.general.hasStrings 133 | hasURLs = UIPasteboard.general.hasURLs 134 | } 135 | 136 | var contents = [ 137 | "urls": [], 138 | "strings": [], 139 | "hasImages": hasImagesInClipboard, 140 | "hasURLs": hasURLs, 141 | "hasStrings": hasStrings 142 | ] as [String : Any] 143 | 144 | if #available(iOS 10.0, *) { 145 | if UIPasteboard.general.hasURLs { 146 | contents["urls"] = UIPasteboard.general.urls?.map { url in 147 | return url.absoluteString 148 | } 149 | } 150 | } else { 151 | // Fallback on earlier versions 152 | } 153 | 154 | if #available(iOS 10.0, *) { 155 | if UIPasteboard.general.hasStrings { 156 | let strings = UIPasteboard.general.strings?.filter { string in 157 | guard let urls = contents["urls"] as? Array else { 158 | return true 159 | } 160 | 161 | return !urls.contains(string) 162 | } 163 | 164 | if (strings?.count ?? 0) > 0 { 165 | contents["strings"] = strings 166 | } else { 167 | contents["hasStrings"] = false 168 | } 169 | } 170 | } else { 171 | // Fallback on earlier versions 172 | } 173 | 174 | return contents 175 | } 176 | 177 | 178 | override func supportedEvents() -> [String]! { 179 | return [ 180 | EventNames.change.rawValue, 181 | EventNames.remove.rawValue 182 | ] 183 | } 184 | 185 | override static func requiresMainQueueSetup() -> Bool { 186 | return false 187 | } 188 | 189 | override func constantsToExport() -> [AnyHashable : Any]! { 190 | return ["clipboard": MediaClipboard.serializeContents(), "mediaSource": self.lastDictionaryValue ?? nil] 191 | } 192 | 193 | @objc(getContent:) 194 | func getContent(_ callback: @escaping RCTResponseSenderBlock) { 195 | callback([nil, MediaClipboard.serializeContents()]) 196 | } 197 | 198 | @objc(lastDictionaryValue) 199 | var lastDictionaryValue: [String: Any]? = nil 200 | var lastSavedImage: UIImage? = nil 201 | 202 | @objc(hasImagesInClipboard) 203 | static var hasImagesInClipboard: Bool { 204 | let imageUTIs = MimeType.images().map {image in 205 | return image.utiType() 206 | } 207 | 208 | return UIPasteboard.general.contains(pasteboardTypes: imageUTIs) 209 | } 210 | 211 | @objc(clipboardMediaSource:) 212 | func clipboardMediaSource(_ callback: @escaping RCTResponseSenderBlock) { 213 | guard MediaClipboard.hasImagesInClipboard else { 214 | callback([nil, [:]]) 215 | return 216 | } 217 | 218 | let image = UIPasteboard.general.image 219 | 220 | if lastSavedImage != nil && lastSavedImage == image && lastDictionaryValue != nil { 221 | callback([nil, lastDictionaryValue!]) 222 | } 223 | 224 | var exportType: MimeType? = nil 225 | if UIPasteboard.general.contains(pasteboardTypes: [MimeType.jpg.utiType()]) { 226 | exportType = MimeType.jpg 227 | } else if UIPasteboard.general.contains(pasteboardTypes: [MimeType.png.utiType()]) { 228 | exportType = MimeType.png 229 | } 230 | 231 | 232 | guard exportType != nil else { 233 | callback([NSError(domain: "com.yeet.react-native-media-clipboard.genericError", code: 111, userInfo: nil)]) 234 | return 235 | } 236 | 237 | DispatchQueue.global(qos: .background).async { 238 | guard let image = image else { 239 | callback([NSError(domain: "com.yeet.react-native-media-clipboard.genericError", code: 111, userInfo: nil)]) 240 | return 241 | } 242 | 243 | guard let exportType = exportType else { 244 | callback([NSError(domain: "com.yeet.react-native-media-clipboard.genericError", code: 111, userInfo: nil)]) 245 | return 246 | } 247 | 248 | let url = self.getExportURL(mimeType: exportType) 249 | var data: Data? = nil 250 | 251 | if exportType == .jpg { 252 | data = image.jpegData(compressionQuality: CGFloat(1.0)) 253 | } else if exportType == .png { 254 | data = image.pngData() 255 | } 256 | 257 | guard data != nil else { 258 | callback([NSError(domain: "com.yeet.react-native-media-clipboard.writingDataError", code: 112, userInfo: nil), nil]) 259 | return 260 | } 261 | 262 | do { 263 | try data?.write(to: url) 264 | } catch { 265 | callback([NSError(domain: "com.yeet.react-native-media-clipboard.writingDataError", code: 112, userInfo: nil), nil]) 266 | return 267 | } 268 | 269 | let size = image.size 270 | 271 | 272 | let value = self.getDictionaryValue(url: url, mimeType: exportType, size: size, scale: image.scale) 273 | self.lastDictionaryValue = value 274 | self.lastSavedImage = image 275 | 276 | callback([nil, value]) 277 | } 278 | } 279 | 280 | open func getExportURL(mimeType: MimeType) -> URL { 281 | return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString.appending(".\(mimeType.fileExtension())")) 282 | } 283 | 284 | 285 | open func getDictionaryValue(url: URL, mimeType: MimeType, size: CGSize, scale: CGFloat) -> [String: Any] { 286 | return [ 287 | "uri": url.absoluteString, 288 | "mimeType": mimeType.rawValue, 289 | "width": size.width, 290 | "height": size.height, 291 | "scale": scale, 292 | ] 293 | } 294 | 295 | deinit { 296 | stopObservingPasteboardChange() 297 | } 298 | 299 | } 300 | 301 | 302 | -------------------------------------------------------------------------------- /ios/MediaClipboard.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8332B0E523F68717003FB121 /* MediaClipboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8332B0E423F68717003FB121 /* MediaClipboard.swift */; }; 11 | 8332B0E823F6878E003FB121 /* MediaClipboardJSI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8332B0E623F6878E003FB121 /* MediaClipboardJSI.mm */; }; 12 | 8332B0EB23F687A6003FB121 /* YeetJSIUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8332B0E923F687A6003FB121 /* YeetJSIUtils.mm */; }; 13 | 8332B0EE23F687F8003FB121 /* MediaClipboardC++Interop.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8332B0ED23F687F8003FB121 /* MediaClipboardC++Interop.mm */; }; 14 | 8332B0F023F68DA3003FB121 /* MimeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8332B0EF23F68DA3003FB121 /* MimeType.swift */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXCopyFilesBuildPhase section */ 18 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 19 | isa = PBXCopyFilesBuildPhase; 20 | buildActionMask = 2147483647; 21 | dstPath = "include/$(PRODUCT_NAME)"; 22 | dstSubfolderSpec = 16; 23 | files = ( 24 | ); 25 | runOnlyForDeploymentPostprocessing = 0; 26 | }; 27 | /* End PBXCopyFilesBuildPhase section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 134814201AA4EA6300B7C361 /* libMediaClipboard.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libMediaClipboard.a; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 8332B0E323F68717003FB121 /* MediaClipboard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MediaClipboard-Bridging-Header.h"; sourceTree = ""; }; 32 | 8332B0E423F68717003FB121 /* MediaClipboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaClipboard.swift; sourceTree = ""; }; 33 | 8332B0E623F6878E003FB121 /* MediaClipboardJSI.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MediaClipboardJSI.mm; sourceTree = ""; }; 34 | 8332B0E723F6878E003FB121 /* MediaClipboardJSI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MediaClipboardJSI.h; sourceTree = ""; }; 35 | 8332B0E923F687A6003FB121 /* YeetJSIUtils.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = YeetJSIUtils.mm; sourceTree = ""; }; 36 | 8332B0EA23F687A6003FB121 /* YeetJSIUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YeetJSIUtils.h; sourceTree = ""; }; 37 | 8332B0EC23F687F8003FB121 /* MediaClipboardC++Interop.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MediaClipboardC++Interop.h"; sourceTree = ""; }; 38 | 8332B0ED23F687F8003FB121 /* MediaClipboardC++Interop.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "MediaClipboardC++Interop.mm"; sourceTree = ""; }; 39 | 8332B0EF23F68DA3003FB121 /* MimeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MimeType.swift; sourceTree = ""; }; 40 | 8332B0F723F6B6EE003FB121 /* MediaClipboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MediaClipboard.h; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 134814211AA4EA7D00B7C361 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 134814201AA4EA6300B7C361 /* libMediaClipboard.a */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 58B511D21A9E6C8500147676 = { 63 | isa = PBXGroup; 64 | children = ( 65 | 8332B0F723F6B6EE003FB121 /* MediaClipboard.h */, 66 | 8332B0E423F68717003FB121 /* MediaClipboard.swift */, 67 | 8332B0EF23F68DA3003FB121 /* MimeType.swift */, 68 | 134814211AA4EA7D00B7C361 /* Products */, 69 | 8332B0E323F68717003FB121 /* MediaClipboard-Bridging-Header.h */, 70 | 8332B0E623F6878E003FB121 /* MediaClipboardJSI.mm */, 71 | 8332B0E923F687A6003FB121 /* YeetJSIUtils.mm */, 72 | 8332B0EA23F687A6003FB121 /* YeetJSIUtils.h */, 73 | 8332B0E723F6878E003FB121 /* MediaClipboardJSI.h */, 74 | 8332B0EC23F687F8003FB121 /* MediaClipboardC++Interop.h */, 75 | 8332B0ED23F687F8003FB121 /* MediaClipboardC++Interop.mm */, 76 | ); 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | 58B511DA1A9E6C8500147676 /* MediaClipboard */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "MediaClipboard" */; 85 | buildPhases = ( 86 | 58B511D71A9E6C8500147676 /* Sources */, 87 | 58B511D81A9E6C8500147676 /* Frameworks */, 88 | 58B511D91A9E6C8500147676 /* CopyFiles */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = MediaClipboard; 95 | productName = RCTDataManager; 96 | productReference = 134814201AA4EA6300B7C361 /* libMediaClipboard.a */; 97 | productType = "com.apple.product-type.library.static"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | 58B511D31A9E6C8500147676 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastUpgradeCheck = 0920; 106 | ORGANIZATIONNAME = Yeet; 107 | TargetAttributes = { 108 | 58B511DA1A9E6C8500147676 = { 109 | CreatedOnToolsVersion = 6.1.1; 110 | LastSwiftMigration = 1130; 111 | }; 112 | }; 113 | }; 114 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "MediaClipboard" */; 115 | compatibilityVersion = "Xcode 3.2"; 116 | developmentRegion = English; 117 | hasScannedForEncodings = 0; 118 | knownRegions = ( 119 | English, 120 | en, 121 | ); 122 | mainGroup = 58B511D21A9E6C8500147676; 123 | productRefGroup = 58B511D21A9E6C8500147676; 124 | projectDirPath = ""; 125 | projectRoot = ""; 126 | targets = ( 127 | 58B511DA1A9E6C8500147676 /* MediaClipboard */, 128 | ); 129 | }; 130 | /* End PBXProject section */ 131 | 132 | /* Begin PBXSourcesBuildPhase section */ 133 | 58B511D71A9E6C8500147676 /* Sources */ = { 134 | isa = PBXSourcesBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | 8332B0EB23F687A6003FB121 /* YeetJSIUtils.mm in Sources */, 138 | 8332B0E823F6878E003FB121 /* MediaClipboardJSI.mm in Sources */, 139 | 8332B0E523F68717003FB121 /* MediaClipboard.swift in Sources */, 140 | 8332B0F023F68DA3003FB121 /* MimeType.swift in Sources */, 141 | 8332B0EE23F687F8003FB121 /* MediaClipboardC++Interop.mm in Sources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXSourcesBuildPhase section */ 146 | 147 | /* Begin XCBuildConfiguration section */ 148 | 58B511ED1A9E6C8500147676 /* Debug */ = { 149 | isa = XCBuildConfiguration; 150 | buildSettings = { 151 | ALWAYS_SEARCH_USER_PATHS = NO; 152 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 153 | CLANG_CXX_LIBRARY = "libc++"; 154 | CLANG_ENABLE_MODULES = YES; 155 | CLANG_ENABLE_OBJC_ARC = YES; 156 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 157 | CLANG_WARN_BOOL_CONVERSION = YES; 158 | CLANG_WARN_COMMA = YES; 159 | CLANG_WARN_CONSTANT_CONVERSION = YES; 160 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 161 | CLANG_WARN_EMPTY_BODY = YES; 162 | CLANG_WARN_ENUM_CONVERSION = YES; 163 | CLANG_WARN_INFINITE_RECURSION = YES; 164 | CLANG_WARN_INT_CONVERSION = YES; 165 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 166 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 167 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 168 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 169 | CLANG_WARN_STRICT_PROTOTYPES = YES; 170 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 171 | CLANG_WARN_UNREACHABLE_CODE = YES; 172 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 173 | COPY_PHASE_STRIP = NO; 174 | ENABLE_STRICT_OBJC_MSGSEND = YES; 175 | ENABLE_TESTABILITY = YES; 176 | GCC_C_LANGUAGE_STANDARD = gnu99; 177 | GCC_DYNAMIC_NO_PIC = NO; 178 | GCC_NO_COMMON_BLOCKS = YES; 179 | GCC_OPTIMIZATION_LEVEL = 0; 180 | GCC_PREPROCESSOR_DEFINITIONS = ( 181 | "DEBUG=1", 182 | "$(inherited)", 183 | ); 184 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 185 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 186 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 187 | GCC_WARN_UNDECLARED_SELECTOR = YES; 188 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 189 | GCC_WARN_UNUSED_FUNCTION = YES; 190 | GCC_WARN_UNUSED_VARIABLE = YES; 191 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 192 | MTL_ENABLE_DEBUG_INFO = YES; 193 | ONLY_ACTIVE_ARCH = YES; 194 | SDKROOT = iphoneos; 195 | }; 196 | name = Debug; 197 | }; 198 | 58B511EE1A9E6C8500147676 /* Release */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 207 | CLANG_WARN_BOOL_CONVERSION = YES; 208 | CLANG_WARN_COMMA = YES; 209 | CLANG_WARN_CONSTANT_CONVERSION = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNREACHABLE_CODE = YES; 222 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 223 | COPY_PHASE_STRIP = YES; 224 | ENABLE_NS_ASSERTIONS = NO; 225 | ENABLE_STRICT_OBJC_MSGSEND = YES; 226 | GCC_C_LANGUAGE_STANDARD = gnu99; 227 | GCC_NO_COMMON_BLOCKS = YES; 228 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 229 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 230 | GCC_WARN_UNDECLARED_SELECTOR = YES; 231 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 232 | GCC_WARN_UNUSED_FUNCTION = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 235 | MTL_ENABLE_DEBUG_INFO = NO; 236 | SDKROOT = iphoneos; 237 | VALIDATE_PRODUCT = YES; 238 | }; 239 | name = Release; 240 | }; 241 | 58B511F01A9E6C8500147676 /* Debug */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | CLANG_ENABLE_MODULES = YES; 245 | HEADER_SEARCH_PATHS = ( 246 | "$(inherited)", 247 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 248 | "$(SRCROOT)/../../../React/**", 249 | "$(SRCROOT)/../../react-native/React/**", 250 | ); 251 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 252 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 253 | OTHER_LDFLAGS = "-ObjC"; 254 | PRODUCT_NAME = MediaClipboard; 255 | SKIP_INSTALL = YES; 256 | SWIFT_OBJC_BRIDGING_HEADER = "MediaClipboard-Bridging-Header.h"; 257 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 258 | SWIFT_VERSION = 5.0; 259 | }; 260 | name = Debug; 261 | }; 262 | 58B511F11A9E6C8500147676 /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | CLANG_ENABLE_MODULES = YES; 266 | HEADER_SEARCH_PATHS = ( 267 | "$(inherited)", 268 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 269 | "$(SRCROOT)/../../../React/**", 270 | "$(SRCROOT)/../../react-native/React/**", 271 | ); 272 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 273 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 274 | OTHER_LDFLAGS = "-ObjC"; 275 | PRODUCT_NAME = MediaClipboard; 276 | SKIP_INSTALL = YES; 277 | SWIFT_OBJC_BRIDGING_HEADER = "MediaClipboard-Bridging-Header.h"; 278 | SWIFT_VERSION = 5.0; 279 | }; 280 | name = Release; 281 | }; 282 | /* End XCBuildConfiguration section */ 283 | 284 | /* Begin XCConfigurationList section */ 285 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "MediaClipboard" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | 58B511ED1A9E6C8500147676 /* Debug */, 289 | 58B511EE1A9E6C8500147676 /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "MediaClipboard" */ = { 295 | isa = XCConfigurationList; 296 | buildConfigurations = ( 297 | 58B511F01A9E6C8500147676 /* Debug */, 298 | 58B511F11A9E6C8500147676 /* Release */, 299 | ); 300 | defaultConfigurationIsVisible = 0; 301 | defaultConfigurationName = Release; 302 | }; 303 | /* End XCConfigurationList section */ 304 | }; 305 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 306 | } 307 | -------------------------------------------------------------------------------- /ios/MediaClipboard.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/MediaClipboard.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/MediaClipboardC++Interop.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaClipboardC++Interop.h 3 | // MediaClipboard 4 | // 5 | // Created by Jarred WSumner on 2/13/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @class MediaClipboard; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @interface MediaClipboardC__Interop : NSObject 17 | 18 | + (void)install:(MediaClipboard*)clipboard; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /ios/MediaClipboardC++Interop.mm: -------------------------------------------------------------------------------- 1 | // 2 | // MediaClipboardC++Interop.m 3 | // MediaClipboard 4 | // 5 | // Created by Jarred WSumner on 2/13/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | #import "MediaClipboardC++Interop.h" 10 | #import "MediaClipboardJSI.h" 11 | 12 | 13 | 14 | @implementation MediaClipboardC__Interop 15 | 16 | +(void)install:(MediaClipboard *)clipboard { 17 | MediaClipboardJSI::install(clipboard); 18 | } 19 | 20 | @end 21 | 22 | -------------------------------------------------------------------------------- /ios/MediaClipboardJSI.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaClipboardJSI.h 3 | // Media 4 | // 5 | // Created by Jarred WSumner on 2/6/20. 6 | // Copyright © 2020 Media. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class MediaClipboard; 12 | 13 | #ifdef __cplusplus 14 | 15 | #include 16 | #import 17 | 18 | 19 | using namespace facebook; 20 | 21 | @class RCTCxxBridge; 22 | 23 | class JSI_EXPORT MediaClipboardJSIModule : public jsi::HostObject { 24 | public: 25 | MediaClipboardJSIModule(MediaClipboard* clipboard); 26 | 27 | static void install(MediaClipboard *clipboard); 28 | 29 | /* 30 | * `jsi::HostObject` specific overloads. 31 | */ 32 | jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override; 33 | 34 | jsi::Value getOther(jsi::Runtime &runtime, const jsi::PropNameID &name); 35 | 36 | private: 37 | MediaClipboard* clipboard_; 38 | std::shared_ptr _jsInvoker; 39 | }; 40 | 41 | #endif 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ios/MediaClipboardJSI.mm: -------------------------------------------------------------------------------- 1 | // 2 | // MediaClipboardJSI.cpp 3 | // MediaClipboard 4 | // 5 | // Created by Jarred WSumner on 2/13/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | #include "MediaClipboardJSI.h" 10 | #import 11 | #import "MediaJSIUTils.h" 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import "RCTConvert+PHotos.h" 17 | #import "MediaClipboardJSI.h" 18 | 19 | 20 | 21 | @interface RCTBridge (ext) 22 | - (std::weak_ptr)reactInstance; 23 | @end 24 | 25 | MediaClipboardJSIModule::MediaClipboardJSIModule 26 | (MediaClipboard *clipboard) 27 | : clipboard_(clipboard) { 28 | std::shared_ptr _jsInvoker = std::make_shared(clipboard.bridge.reactInstance); 29 | } 30 | 31 | 32 | void MediaClipboardJSIModule::install(MediaClipboard *clipboard) { 33 | RCTCxxBridge* bridge = clipboard.bridge; 34 | 35 | if (bridge.runtime == nullptr) { 36 | return; 37 | } 38 | 39 | jsi::Runtime &runtime = *(jsi::Runtime *)bridge.runtime; 40 | 41 | auto reaModuleName = "Clipboard"; 42 | auto reaJsiModule = std::make_shared(std::move(clipboard)); 43 | auto object = jsi::Object::createFromHostObject(runtime, reaJsiModule); 44 | runtime.global().setProperty(runtime, reaModuleName, std::move(object)); 45 | } 46 | 47 | jsi::Value MediaClipboardJSIModule::get(jsi::Runtime &runtime, const jsi::PropNameID &name) { 48 | if (_jsInvoker == nullptr) { 49 | RCTCxxBridge* bridge = clipboard_.bridge; 50 | _jsInvoker = std::make_shared(bridge.reactInstance); 51 | } 52 | 53 | 54 | auto methodName = name.utf8(runtime); 55 | 56 | if (methodName == "getMediaSource") { 57 | MediaClipboard *clipboard = clipboard_; 58 | std::shared_ptr jsInvoker = _jsInvoker; 59 | 60 | return jsi::Function::createFromHostFunction(runtime, name, 1, [clipboard, jsInvoker]( 61 | jsi::Runtime &runtime, 62 | const jsi::Value &thisValue, 63 | const jsi::Value *arguments, 64 | size_t count) -> jsi::Value { 65 | 66 | // Promise return type is special cased today, i.e. it needs extra 2 function args for resolve() and reject(), to 67 | // be passed to the actual ObjC++ class method. 68 | return createPromise(runtime, jsInvoker, ^(jsi::Runtime &rt, std::shared_ptr wrapper) { 69 | NSMutableArray *retained = [[NSMutableArray alloc] initWithCapacity:2]; 70 | if (clipboard.lastMediaSource) { 71 | wrapper->resolveBlock()(clipboard.lastMediaSource.toDictionary); 72 | } else if (UIPasteboard.generalPasteboard.hasImages) { 73 | RCTPromiseResolveBlock resolver = wrapper->resolveBlock(); 74 | RCTPromiseRejectBlock rejecter = wrapper->rejectBlock(); 75 | [retained addObject:resolver]; 76 | [retained addObject:rejecter]; 77 | 78 | [clipboard clipboardMediaSource:^(NSArray *response) { 79 | NSError *error = [response objectAtIndex:0]; 80 | NSDictionary *value = [response objectAtIndex:1]; 81 | if (error && error != [NSNull null]) { 82 | rejecter([NSString stringWithFormat:@"%ldu", (long)error.code], error.domain, error); 83 | } else { 84 | resolver(value); 85 | } 86 | 87 | [retained removeAllObjects]; 88 | }]; 89 | } else { 90 | wrapper->resolveBlock()(nil); 91 | } 92 | }); 93 | }); 94 | 95 | } 96 | 97 | return jsi::Value::undefined(); 98 | } 99 | -------------------------------------------------------------------------------- /ios/MimeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MimeType.swift 3 | // MediaClipboard 4 | // 5 | // Created by Jarred WSumner on 2/14/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MobileCoreServices 11 | 12 | enum MimeType: String { 13 | case png = "image/png" 14 | case gif = "image/gif" 15 | case webp = "image/webp" 16 | case jpg = "image/jpeg" 17 | case mp4 = "video/mp4" 18 | case m4v = "video/x-m4v" 19 | case heic = "image/heic" 20 | case heif = "image/heif" 21 | case tiff = "image/tiff" 22 | case mov = "video/quicktime" 23 | case bmp = "image/bmp" 24 | 25 | static func images() -> Array { 26 | return [ 27 | MimeType.heic, 28 | MimeType.webp, 29 | MimeType.jpg, 30 | MimeType.heif, 31 | MimeType.tiff, 32 | MimeType.bmp, 33 | MimeType.png, 34 | ] 35 | } 36 | 37 | func utiType() -> String { 38 | switch self { 39 | 40 | case .png: 41 | return "public.png" 42 | case .gif: 43 | return "public.gif" 44 | case .webp: 45 | return "public.webp" 46 | case .jpg: 47 | return "public.jpeg" 48 | case .mp4: 49 | return "public.mpeg-4" 50 | case .m4v: 51 | return "public.mpeg-4" 52 | case .heic: 53 | return "public.heic" 54 | 55 | case .heif: 56 | return "public.heif" 57 | case .tiff: 58 | return "public.tiff" 59 | case .mov: 60 | return "com.apple.quicktime-movie" 61 | case .bmp: 62 | return "public.bmp" 63 | } 64 | } 65 | 66 | static func from(uti: String) -> MimeType? { 67 | switch uti { 68 | case "public.png": 69 | return MimeType.png 70 | case "public.gif": 71 | return MimeType.gif 72 | case "public.webp": 73 | return MimeType.webp 74 | case "public.jpeg": 75 | return MimeType.jpg 76 | case "public.mpeg-4": 77 | return MimeType.mp4 78 | 79 | case "public.heic": 80 | return MimeType.heic 81 | 82 | case "public.heif": 83 | return MimeType.heif 84 | case "public.tiff": 85 | return MimeType.tiff 86 | case "com.apple.quicktime-movie": 87 | return MimeType.mov 88 | case "public.bmp": 89 | return MimeType.bmp 90 | default: 91 | return nil 92 | } 93 | } 94 | 95 | func fileExtension() -> String { 96 | switch self { 97 | case .png: 98 | return "png" 99 | case .gif: 100 | return "gif" 101 | case .m4v: 102 | return "m4v" 103 | case .webp: 104 | return "webp" 105 | case .jpg: 106 | return "jpg" 107 | case .mp4: 108 | return "mp4" 109 | 110 | case .heic: 111 | return "heic" 112 | 113 | case .heif: 114 | return "heif" 115 | case .tiff: 116 | return "tiff" 117 | case .mov: 118 | return "mov" 119 | case .bmp: 120 | return "bmp" 121 | } 122 | } 123 | 124 | static func url(_ url: URL) -> MimeType? { 125 | return fileExtension(url.pathExtension) 126 | } 127 | 128 | func isAnimatable() -> Bool { 129 | return [.gif, .webp].contains(self) 130 | } 131 | 132 | static func fileExtension(_ ext: String) -> MimeType? { 133 | 134 | 135 | let fileExtension = ext as CFString 136 | 137 | guard 138 | let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue() 139 | else { return nil } 140 | 141 | guard 142 | let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) 143 | else { return nil } 144 | 145 | return MimeType(rawValue: mimeUTI.takeUnretainedValue() as String) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ios/YeetJSIUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // YeetJSIUTils.h 3 | // yeet 4 | // 5 | // Created by Jarred WSumner on 1/30/20. 6 | // Copyright © 2020 Yeet. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | #ifdef __cplusplus 11 | 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | #import 18 | #import 19 | 20 | using namespace facebook; 21 | /** 22 | * All static helper functions are ObjC++ specific. 23 | // */ 24 | jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); 25 | jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value); 26 | jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value); 27 | jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value); 28 | jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value); 29 | jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value); 30 | //std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value); 31 | //jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); 32 | //id convertJSIValueToObjCObject( 33 | // jsi::Runtime &runtime, 34 | // const jsi::Value &value); 35 | //NSString* convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value); 36 | //NSArray* convertJSIArrayToNSArray( 37 | // jsi::Runtime &runtime, 38 | // const jsi::Array &value); 39 | //NSDictionary *convertJSIObjectToNSDictionary( 40 | // jsi::Runtime &runtime, 41 | // const jsi::Object &value); 42 | 43 | 44 | NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value); 45 | NSArray *convertJSIArrayToNSArray( 46 | jsi::Runtime &runtime, 47 | const jsi::Array &value, 48 | std::shared_ptr jsInvoker 49 | ); 50 | NSDictionary *convertJSIObjectToNSDictionary( 51 | jsi::Runtime &runtime, 52 | const jsi::Object &value, 53 | std::shared_ptr jsInvoker); 54 | RCTResponseSenderBlock convertJSIFunctionToCallback( 55 | jsi::Runtime &runtime, 56 | const jsi::Function &value, 57 | std::shared_ptr jsInvoker); 58 | id convertJSIValueToObjCObject( 59 | jsi::Runtime &runtime, 60 | const jsi::Value &value, 61 | std::shared_ptr jsInvoker); 62 | 63 | // Helper for creating Promise object. 64 | struct PromiseWrapper : public react::LongLivedObject { 65 | static std::shared_ptr create( 66 | jsi::Function resolve, 67 | jsi::Function reject, 68 | jsi::Runtime &runtime, 69 | std::shared_ptr jsInvoker) 70 | { 71 | auto instance = std::make_shared(std::move(resolve), std::move(reject), runtime, jsInvoker); 72 | // This instance needs to live longer than the caller's scope, since the resolve/reject functions may not 73 | // be called immediately. Doing so keeps it alive at least until resolve/reject is called, or when the 74 | // collection is cleared (e.g. when JS reloads). 75 | react::LongLivedObjectCollection::get().add(instance); 76 | return instance; 77 | } 78 | 79 | PromiseWrapper( 80 | jsi::Function resolve, 81 | jsi::Function reject, 82 | jsi::Runtime &runtime, 83 | std::shared_ptr jsInvoker) 84 | : resolveWrapper(std::make_shared(std::move(resolve), runtime, jsInvoker)), 85 | rejectWrapper(std::make_shared(std::move(reject), runtime, jsInvoker)), 86 | runtime(runtime), 87 | jsInvoker(jsInvoker) 88 | { 89 | } 90 | 91 | RCTPromiseResolveBlock resolveBlock() 92 | { 93 | return ^(id result) { 94 | if (resolveWrapper == nullptr) { 95 | throw std::runtime_error("Promise resolve arg cannot be called more than once"); 96 | } 97 | 98 | // Retain the resolveWrapper so that it stays alive inside the lambda. 99 | std::shared_ptr retainedWrapper = resolveWrapper; 100 | std::shared_ptr invoker = jsInvoker; 101 | jsInvoker->invokeAsync([retainedWrapper, result, invoker]() { 102 | jsi::Runtime &rt = retainedWrapper->runtime(); 103 | jsi::Value arg = convertObjCObjectToJSIValue(rt, result); 104 | retainedWrapper->callback().call(rt, arg); 105 | }); 106 | 107 | // Prevent future invocation of the same resolve() function. 108 | cleanup(); 109 | }; 110 | } 111 | 112 | RCTPromiseRejectBlock rejectBlock() 113 | { 114 | return ^(NSString *code, NSString *message, NSError *error) { 115 | // TODO: There is a chance `this` is no longer valid when this block executes. 116 | if (rejectWrapper == nullptr) { 117 | throw std::runtime_error("Promise reject arg cannot be called more than once"); 118 | } 119 | 120 | // Retain the resolveWrapper so that it stays alive inside the lambda. 121 | std::shared_ptr retainedWrapper = rejectWrapper; 122 | NSDictionary *jsError = RCTJSErrorFromCodeMessageAndNSError(code, message, error); 123 | jsInvoker->invokeAsync([retainedWrapper, jsError]() { 124 | jsi::Runtime &rt = retainedWrapper->runtime(); 125 | jsi::Value arg = convertNSDictionaryToJSIObject(rt, jsError); 126 | retainedWrapper->callback().call(rt, arg); 127 | }); 128 | 129 | // Prevent future invocation of the same resolve() function. 130 | cleanup(); 131 | }; 132 | } 133 | 134 | void cleanup() 135 | { 136 | resolveWrapper = nullptr; 137 | rejectWrapper = nullptr; 138 | allowRelease(); 139 | } 140 | 141 | // CallbackWrapper is used here instead of just holding on the jsi jsi::Function in order to force release it after 142 | // either the resolve() or the reject() is called. jsi jsi::Function does not support explicit releasing, so we need 143 | // an extra mechanism to control that lifecycle. 144 | std::shared_ptr resolveWrapper; 145 | std::shared_ptr rejectWrapper; 146 | jsi::Runtime &runtime; 147 | std::shared_ptr jsInvoker; 148 | }; 149 | 150 | using PromiseInvocationBlock = void (^)(jsi::Runtime &rt, std::shared_ptr wrapper); 151 | jsi::Value 152 | createPromise(jsi::Runtime &runtime, std::shared_ptr jsInvoker, PromiseInvocationBlock invoke); 153 | 154 | #endif 155 | -------------------------------------------------------------------------------- /ios/YeetJSIUtils.mm: -------------------------------------------------------------------------------- 1 | // 2 | // REAJsiUtilities.cpp 3 | // RNReanimated 4 | // 5 | // Created by Christian Falch on 25/04/2019. 6 | // Copyright © 2019 Yeet. All rights reserved. 7 | // 8 | 9 | #include "YeetJSIUTils.h" 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) 16 | { 17 | if ([value isKindOfClass:[NSString class]]) { 18 | return convertNSStringToJSIString(runtime, (NSString *)value); 19 | } else if ([value isKindOfClass:[NSNumber class]]) { 20 | if ([value isKindOfClass:[@YES class]]) { 21 | return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); 22 | } 23 | return convertNSNumberToJSINumber(runtime, (NSNumber *)value); 24 | } else if ([value isKindOfClass:[NSDictionary class]]) { 25 | return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value); 26 | } else if ([value isKindOfClass:[NSArray class]]) { 27 | return convertNSArrayToJSIArray(runtime, (NSArray *)value); 28 | } else if (value == (id)kCFNull) { 29 | return jsi::Value::null(); 30 | } 31 | return jsi::Value::undefined(); 32 | } 33 | 34 | 35 | jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) 36 | { 37 | return jsi::Value((bool)[value boolValue]); 38 | } 39 | 40 | jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) 41 | { 42 | return jsi::Value([value doubleValue]); 43 | } 44 | 45 | jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) 46 | { 47 | return jsi::String::createFromUtf8(runtime, [value UTF8String] ?: ""); 48 | } 49 | 50 | jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) 51 | { 52 | jsi::Object result = jsi::Object(runtime); 53 | for (NSString *k in value) { 54 | result.setProperty(runtime, [k UTF8String], convertObjCObjectToJSIValue(runtime, value[k])); 55 | } 56 | return result; 57 | } 58 | 59 | jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) 60 | { 61 | jsi::Array result = jsi::Array(runtime, value.count); 62 | for (size_t i = 0; i < value.count; i++) { 63 | result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); 64 | } 65 | return result; 66 | } 67 | 68 | std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) 69 | { 70 | std::vector result; 71 | for (size_t i = 0; i < value.count; i++) { 72 | result.emplace_back(convertObjCObjectToJSIValue(runtime, value[i])); 73 | } 74 | return result; 75 | } 76 | 77 | NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) 78 | { 79 | return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; 80 | } 81 | 82 | NSArray *convertJSIArrayToNSArray( 83 | jsi::Runtime &runtime, 84 | const jsi::Array &value, 85 | std::shared_ptr jsInvoker) 86 | { 87 | size_t size = value.size(runtime); 88 | NSMutableArray *result = [NSMutableArray new]; 89 | for (size_t i = 0; i < size; i++) { 90 | // Insert kCFNull when it's `undefined` value to preserve the indices. 91 | [result 92 | addObject:convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker) ?: (id)kCFNull]; 93 | } 94 | return [result copy]; 95 | } 96 | 97 | NSDictionary *convertJSIObjectToNSDictionary( 98 | jsi::Runtime &runtime, 99 | const jsi::Object &value, 100 | std::shared_ptr jsInvoker) 101 | { 102 | jsi::Array propertyNames = value.getPropertyNames(runtime); 103 | size_t size = propertyNames.size(runtime); 104 | NSMutableDictionary *result = [NSMutableDictionary new]; 105 | for (size_t i = 0; i < size; i++) { 106 | jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime); 107 | NSString *k = convertJSIStringToNSString(runtime, name); 108 | id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker); 109 | if (v) { 110 | result[k] = v; 111 | } 112 | } 113 | return [result copy]; 114 | } 115 | 116 | 117 | 118 | 119 | RCTResponseSenderBlock convertJSIFunctionToCallback( 120 | jsi::Runtime &runtime, 121 | const jsi::Function &value, 122 | std::shared_ptr jsInvoker) 123 | { 124 | __block auto wrapper = std::make_shared(value.getFunction(runtime), runtime, jsInvoker); 125 | return ^(NSArray *responses) { 126 | if (wrapper == nullptr) { 127 | throw std::runtime_error("callback arg cannot be called more than once"); 128 | } 129 | 130 | std::shared_ptr rw = wrapper; 131 | wrapper->jsInvoker().invokeAsync([rw, responses]() { 132 | std::vector args = convertNSArrayToStdVector(rw->runtime(), responses); 133 | rw->callback().call(rw->runtime(), (const jsi::Value *)args.data(), args.size()); 134 | }); 135 | 136 | // The callback is single-use, so force release it here. 137 | // Doing this also releases the jsi::jsi::Function early, since this block may not get released by ARC for a while, 138 | // because the method invocation isn't guarded with @autoreleasepool. 139 | wrapper = nullptr; 140 | }; 141 | } 142 | 143 | 144 | using PromiseInvocationBlock = void (^)(jsi::Runtime &rt, std::shared_ptr wrapper); 145 | 146 | 147 | jsi::Value 148 | createPromise(jsi::Runtime &runtime, std::shared_ptr jsInvoker, PromiseInvocationBlock invoke) 149 | { 150 | if (!invoke) { 151 | return jsi::Value::undefined(); 152 | } 153 | 154 | jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise"); 155 | 156 | // Note: the passed invoke() block is not retained by default, so let's retain it here to help keep it longer. 157 | // Otherwise, there's a risk of it getting released before the promise function below executes. 158 | PromiseInvocationBlock invokeCopy = [invoke copy]; 159 | jsi::Function fn = jsi::Function::createFromHostFunction( 160 | runtime, 161 | jsi::PropNameID::forAscii(runtime, "fn"), 162 | 2, 163 | [invokeCopy, jsInvoker](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { 164 | if (count != 2) { 165 | throw std::invalid_argument("Promise fn arg count must be 2"); 166 | } 167 | if (!invokeCopy) { 168 | return jsi::Value::undefined(); 169 | } 170 | jsi::Function resolve = args[0].getObject(rt).getFunction(rt); 171 | jsi::Function reject = args[1].getObject(rt).getFunction(rt); 172 | auto wrapper = PromiseWrapper::create(std::move(resolve), std::move(reject), rt, jsInvoker); 173 | invokeCopy(rt, wrapper); 174 | return jsi::Value::undefined(); 175 | }); 176 | 177 | return Promise.callAsConstructor(runtime, fn); 178 | } 179 | 180 | 181 | 182 | 183 | 184 | // 185 | ///** 186 | // * All helper functions are ObjC++ specific. 187 | // */ 188 | 189 | // 190 | //jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) 191 | //{ 192 | // if ([value isKindOfClass:[NSString class]]) { 193 | // return convertNSStringToJSIString(runtime, (NSString *)value); 194 | // } else if ([value isKindOfClass:[NSNumber class]]) { 195 | // if ([value isKindOfClass:[@YES class]]) { 196 | // return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); 197 | // } 198 | // return convertNSNumberToJSINumber(runtime, (NSNumber *)value); 199 | // } else if ([value isKindOfClass:[NSDictionary class]]) { 200 | // return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value); 201 | // } else if ([value isKindOfClass:[NSArray class]]) { 202 | // return convertNSArrayToJSIArray(runtime, (NSArray *)value); 203 | // } else if (value == (id)kCFNull) { 204 | // return jsi::Value::null(); 205 | // } 206 | // return jsi::Value::undefined(); 207 | //} 208 | // 209 | 210 | 211 | //NSArray *convertJSIArrayToNSArray( 212 | // jsi::Runtime &runtime, 213 | // const jsi::Array &value) 214 | //{ 215 | // size_t size = value.size(runtime); 216 | // NSMutableArray *result = [NSMutableArray new]; 217 | // for (size_t i = 0; i < size; i++) { 218 | // // Insert kCFNull when it's `undefined` value to preserve the indices. 219 | // [result 220 | // addObject:convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i)) ?: (id)kCFNull]; 221 | // } 222 | // return [result copy]; 223 | //} 224 | // 225 | //NSDictionary *convertJSIObjectToNSDictionary( 226 | // jsi::Runtime &runtime, 227 | // const jsi::Object &value) 228 | //{ 229 | // jsi::Array propertyNames = value.getPropertyNames(runtime); 230 | // size_t size = propertyNames.size(runtime); 231 | // NSMutableDictionary *result = [NSMutableDictionary new]; 232 | // for (size_t i = 0; i < size; i++) { 233 | // jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime); 234 | // NSString *k = convertJSIStringToNSString(runtime, name); 235 | // id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name)); 236 | // if (v) { 237 | // result[k] = v; 238 | // } 239 | // } 240 | // return [result copy]; 241 | //} 242 | // 243 | //RCTResponseSenderBlock convertJSIFunctionToCallback( 244 | // jsi::Runtime &runtime, 245 | // const jsi::Function &value) 246 | //{ 247 | // __block auto cb = value.getFunction(runtime); 248 | // 249 | // return ^(NSArray *responses) { 250 | // cb.call(runtime, convertNSArrayToJSIArray(runtime, responses), 2); 251 | // }; 252 | //} 253 | // 254 | //id convertJSIValueToObjCObject( 255 | // jsi::Runtime &runtime, 256 | // const jsi::Value &value) 257 | //{ 258 | // if (value.isUndefined() || value.isNull()) { 259 | // return nil; 260 | // } 261 | // if (value.isBool()) { 262 | // return @(value.getBool()); 263 | // } 264 | // if (value.isNumber()) { 265 | // return @(value.getNumber()); 266 | // } 267 | // if (value.isString()) { 268 | // return convertJSIStringToNSString(runtime, value.getString(runtime)); 269 | // } 270 | // if (value.isObject()) { 271 | // jsi::Object o = value.getObject(runtime); 272 | // if (o.isArray(runtime)) { 273 | // return convertJSIArrayToNSArray(runtime, o.getArray(runtime)); 274 | // } 275 | // if (o.isFunction(runtime)) { 276 | // return convertJSIFunctionToCallback(runtime, std::move(o.getFunction(runtime))); 277 | // } 278 | // return convertJSIObjectToNSDictionary(runtime, o); 279 | // } 280 | // 281 | // throw std::runtime_error("Unsupported jsi::jsi::Value kind"); 282 | //} 283 | // 284 | //static id convertJSIValueToObjCObject( 285 | // jsi::Runtime &runtime, 286 | // const jsi::Value &value, 287 | // std::shared_ptr jsInvoker); 288 | //static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value); 289 | //static NSArray *convertJSIArrayToNSArray( 290 | // jsi::Runtime &runtime, 291 | // const jsi::Array &value, 292 | // std::shared_ptr jsInvoker 293 | //); 294 | //static NSDictionary *convertJSIObjectToNSDictionary( 295 | // jsi::Runtime &runtime, 296 | // const jsi::Object &value, 297 | // std::shared_ptr jsInvoker); 298 | //static RCTResponseSenderBlock convertJSIFunctionToCallback( 299 | // jsi::Runtime &runtime, 300 | // const jsi::Function &value, 301 | // std::shared_ptr jsInvoker); 302 | //static id convertJSIValueToObjCObject( 303 | // jsi::Runtime &runtime, 304 | // const jsi::Value &value, 305 | // std::shared_ptr jsInvoker); 306 | //static RCTResponseSenderBlock convertJSIFunctionToCallback( 307 | // jsi::Runtime &runtime, 308 | // const jsi::Function &value, 309 | // std::shared_ptr jsInvoker); 310 | // 311 | //// Helper for creating Promise object. 312 | //struct PromiseWrapper : public react::LongLivedObject { 313 | // static std::shared_ptr create( 314 | // jsi::Function resolve, 315 | // jsi::Function reject, 316 | // jsi::Runtime &runtime, 317 | // std::shared_ptr jsInvoker) 318 | // { 319 | // auto instance = std::make_shared(std::move(resolve), std::move(reject), runtime, jsInvoker); 320 | // // This instance needs to live longer than the caller's scope, since the resolve/reject functions may not 321 | // // be called immediately. Doing so keeps it alive at least until resolve/reject is called, or when the 322 | // // collection is cleared (e.g. when JS reloads). 323 | // react::LongLivedObjectCollection::get().add(instance); 324 | // return instance; 325 | // } 326 | // 327 | // PromiseWrapper( 328 | // jsi::Function resolve, 329 | // jsi::Function reject, 330 | // jsi::Runtime &runtime, 331 | // std::shared_ptr jsInvoker) 332 | // : resolveWrapper(std::make_shared(std::move(resolve), runtime, jsInvoker)), 333 | // rejectWrapper(std::make_shared(std::move(reject), runtime, jsInvoker)), 334 | // runtime(runtime), 335 | // jsInvoker(jsInvoker) 336 | // { 337 | // } 338 | // 339 | // RCTPromiseResolveBlock resolveBlock() 340 | // { 341 | // return ^(id result) { 342 | // if (resolveWrapper == nullptr) { 343 | // throw std::runtime_error("Promise resolve arg cannot be called more than once"); 344 | // } 345 | // 346 | // // Retain the resolveWrapper so that it stays alive inside the lambda. 347 | // std::shared_ptr retainedWrapper = resolveWrapper; 348 | // jsInvoker->invokeAsync([retainedWrapper, result]() { 349 | // jsi::Runtime &rt = retainedWrapper->runtime(); 350 | // jsi::Value arg = convertObjCObjectToJSIValue(rt, result); 351 | // retainedWrapper->callback().call(rt, arg); 352 | // }); 353 | // 354 | // // Prevent future invocation of the same resolve() function. 355 | // cleanup(); 356 | // }; 357 | // } 358 | // 359 | // RCTPromiseRejectBlock rejectBlock() 360 | // { 361 | // return ^(NSString *code, NSString *message, NSError *error) { 362 | // // TODO: There is a chance `this` is no longer valid when this block executes. 363 | // if (rejectWrapper == nullptr) { 364 | // throw std::runtime_error("Promise reject arg cannot be called more than once"); 365 | // } 366 | // 367 | // // Retain the resolveWrapper so that it stays alive inside the lambda. 368 | // std::shared_ptr retainedWrapper = rejectWrapper; 369 | // NSDictionary *jsError = RCTJSErrorFromCodeMessageAndNSError(code, message, error); 370 | // jsInvoker->invokeAsync([retainedWrapper, jsError]() { 371 | // jsi::Runtime &rt = retainedWrapper->runtime(); 372 | // jsi::Value arg = convertNSDictionaryToJSIObject(rt, jsError); 373 | // retainedWrapper->callback().call(rt, arg); 374 | // }); 375 | // 376 | // // Prevent future invocation of the same resolve() function. 377 | // cleanup(); 378 | // }; 379 | // } 380 | // 381 | // void cleanup() 382 | // { 383 | // resolveWrapper = nullptr; 384 | // rejectWrapper = nullptr; 385 | // allowRelease(); 386 | // } 387 | // 388 | // // CallbackWrapper is used here instead of just holding on the jsi jsi::Function in order to force release it after 389 | // // either the resolve() or the reject() is called. jsi jsi::Function does not support explicit releasing, so we need 390 | // // an extra mechanism to control that lifecycle. 391 | // std::shared_ptr resolveWrapper; 392 | // std::shared_ptr rejectWrapper; 393 | // jsi::Runtime &runtime; 394 | // std::shared_ptr jsInvoker; 395 | //}; 396 | // 397 | //using PromiseInvocationBlock = void (^)(jsi::Runtime &rt, std::shared_ptr wrapper); 398 | //static jsi::Value 399 | //createPromise(jsi::Runtime &runtime, std::shared_ptr jsInvoker, PromiseInvocationBlock invoke) 400 | //{ 401 | // if (!invoke) { 402 | // return jsi::Value::undefined(); 403 | // } 404 | // 405 | // jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise"); 406 | // 407 | // // Note: the passed invoke() block is not retained by default, so let's retain it here to help keep it longer. 408 | // // Otherwise, there's a risk of it getting released before the promise function below executes. 409 | // PromiseInvocationBlock invokeCopy = [invoke copy]; 410 | // jsi::Function fn = jsi::Function::createFromHostFunction( 411 | // runtime, 412 | // jsi::PropNameID::forAscii(runtime, "fn"), 413 | // 2, 414 | // [invokeCopy, jsInvoker](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { 415 | // if (count != 2) { 416 | // throw std::invalid_argument("Promise fn arg count must be 2"); 417 | // } 418 | // if (!invokeCopy) { 419 | // return jsi::Value::undefined(); 420 | // } 421 | // jsi::Function resolve = args[0].getObject(rt).getFunction(rt); 422 | // jsi::Function reject = args[1].getObject(rt).getFunction(rt); 423 | // auto wrapper = PromiseWrapper::create(std::move(resolve), std::move(reject), rt, jsInvoker); 424 | // invokeCopy(rt, wrapper); 425 | // return jsi::Value::undefined(); 426 | // }); 427 | // 428 | // return Promise.callAsConstructor(runtime, fn); 429 | //} 430 | 431 | 432 | id convertJSIValueToObjCObject( 433 | jsi::Runtime &runtime, 434 | const jsi::Value &value, 435 | std::shared_ptr jsInvoker) 436 | { 437 | if (value.isUndefined() || value.isNull()) { 438 | return nil; 439 | } 440 | if (value.isBool()) { 441 | return @(value.getBool()); 442 | } 443 | if (value.isNumber()) { 444 | return @(value.getNumber()); 445 | } 446 | if (value.isString()) { 447 | return convertJSIStringToNSString(runtime, value.getString(runtime)); 448 | } 449 | if (value.isObject()) { 450 | jsi::Object o = value.getObject(runtime); 451 | if (o.isArray(runtime)) { 452 | return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker); 453 | } 454 | if (o.isFunction(runtime)) { 455 | return convertJSIFunctionToCallback(runtime, std::move(o.getFunction(runtime)), jsInvoker); 456 | } 457 | return convertJSIObjectToNSDictionary(runtime, o, jsInvoker); 458 | } 459 | 460 | throw std::runtime_error("Unsupported jsi::jsi::Value kind"); 461 | } 462 | 463 | 464 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-media-clipboard", 3 | "title": "Clipboard for React Native with images", 4 | "version": "1.0.3", 5 | "description": "TODO", 6 | "scripts": { 7 | "prepare": "bob build", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "module": "lib/module/index.js", 11 | "react-native": "src/index.ts", 12 | "types": "lib/typescript/src/index.d.ts", 13 | "files": [ 14 | "lib/", 15 | "src/", 16 | "react-native-media-clipboard.podspec", 17 | "ios/*.swift", 18 | "ios/*.xcodeproj", 19 | "ios/*.mm", 20 | "ios/*.m", 21 | "ios/*.h", 22 | "android" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Jarred-Sumner/react-native-media-clipboard.git", 27 | "baseUrl": "https://github.com/Jarred-Sumner/react-native-media-clipboard" 28 | }, 29 | "keywords": [ 30 | "react-native" 31 | ], 32 | "author": { 33 | "name": "Jarred Sumner", 34 | "email": "jarred@jaredsumner.com" 35 | }, 36 | "@react-native-community/bob": { 37 | "source": "src", 38 | "output": "lib", 39 | "targets": [ 40 | [ 41 | "commonjs", 42 | { 43 | "copyFlow": true 44 | } 45 | ], 46 | "module", 47 | "typescript" 48 | ] 49 | }, 50 | "license": "MIT", 51 | "licenseFilename": "LICENSE", 52 | "readmeFilename": "README.md", 53 | "peerDependencies": { 54 | "react": "^16.8.1", 55 | "react-native": ">=0.60.0-rc.0 <1.0.x" 56 | }, 57 | "devDependencies": { 58 | "@react-native-community/bob": "^0.9.7", 59 | "react": "^16.9.0", 60 | "react-native": "^0.61.5" 61 | }, 62 | "dependencies": { 63 | "lodash": "^4.17.15" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /react-native-media-clipboard.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | 6 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 7 | folly_version = '2018.10.22.00' 8 | 9 | Pod::Spec.new do |s| 10 | s.name = "react-native-media-clipboard" 11 | s.version = package["version"] 12 | s.summary = package["description"] 13 | s.description = <<-DESC 14 | react-native-media-clipboard 15 | DESC 16 | s.homepage = "https://github.com/github_account/react-native-media-clipboard" 17 | s.license = "MIT" 18 | # s.license = { :type => "MIT", :file => "FILE_LICENSE" } 19 | s.authors = { "Your Name" => "yourname@email.com" } 20 | s.platforms = { :ios => "9.0" } 21 | s.source = { :git => "https://github.com/github_account/react-native-media-clipboard.git", :tag => "#{s.version}" } 22 | 23 | s.source_files = "ios/**/*.{h,m,swift}" 24 | s.requires_arc = true 25 | s.default_subspec = 'Bridge' 26 | 27 | 28 | s.swift_version = '5.0' 29 | 30 | s.dependency "React" 31 | 32 | 33 | s.subspec 'Bridge' do |lite| 34 | 35 | lite.pod_target_xcconfig = { "LIBRAY_SEARCH_PATHS" => "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"" } 36 | end 37 | 38 | s.subspec 'JSI' do |jsi| 39 | jsi.source_files = "ios/**/*.{h,m,swift,mm}" 40 | jsi.dependency "React-jsi" 41 | jsi.dependency "React-jsiexecutor" 42 | jsi.dependency "ReactCommon/jscallinvoker" 43 | jsi.dependency 'ReactCommon/turbomodule/core' 44 | jsi.dependency 'React-cxxreact' 45 | jsi.dependency 'Folly' 46 | jsi.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Folly\"", 'DEFINES_MODULE' => 'YES', 'ENABLE_BITCODE' => "NO", "LIBRAY_SEARCH_PATHS" => "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"" } 47 | jsi.compiler_flags = folly_compiler_flags 48 | end 49 | 50 | 51 | end 52 | 53 | -------------------------------------------------------------------------------- /scripts/examples_postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Using libraries within examples and linking them within packages.json like: 5 | * "react-native-library-name": "file:../" 6 | * will cause problems with the metro bundler if the example will run via 7 | * `react-native run-[ios|android]`. This will result in an error as the metro 8 | * bundler will find multiple versions for the same module while resolving it. 9 | * The reason for that is that if the library is installed it also copies in the 10 | * example folder itself as well as the node_modules folder of the library 11 | * although their are defined in .npmignore and should be ignored in theory. 12 | * 13 | * This postinstall script removes the node_modules folder as well as all 14 | * entries from the libraries .npmignore file within the examples node_modules 15 | * folder after the library was installed. This should resolve the metro 16 | * bundler issue mentioned above. 17 | * 18 | * It is expected this scripts lives in the libraries root folder within a 19 | * scripts folder. As first parameter the relative path to the libraries 20 | * folder within the example's node_modules folder may be provided. 21 | * This script will determine the path from this project's package.json file 22 | * if no such relative path is provided. 23 | * An example's package.json entry could look like: 24 | * "postinstall": "node ../scripts/examples_postinstall.js node_modules/react-native-library-name/" 25 | */ 26 | 27 | 'use strict'; 28 | 29 | const fs = require('fs'); 30 | const path = require('path'); 31 | 32 | /// Delete all files and directories for the given path 33 | const removeFileDirectoryRecursively = fileDirPath => { 34 | // Remove file 35 | if (!fs.lstatSync(fileDirPath).isDirectory()) { 36 | fs.unlinkSync(fileDirPath); 37 | return; 38 | } 39 | 40 | // Go down the directory an remove each file / directory recursively 41 | fs.readdirSync(fileDirPath).forEach(entry => { 42 | const entryPath = path.join(fileDirPath, entry); 43 | removeFileDirectoryRecursively(entryPath); 44 | }); 45 | fs.rmdirSync(fileDirPath); 46 | }; 47 | 48 | /// Remove example/node_modules/react-native-library-name/node_modules directory 49 | const removeLibraryNodeModulesPath = (libraryNodeModulesPath) => { 50 | const nodeModulesPath = path.resolve(libraryNodeModulesPath, 'node_modules') 51 | 52 | if (!fs.existsSync(nodeModulesPath)) { 53 | console.log(`No node_modules path found at ${nodeModulesPath}. Skipping delete.`) 54 | return; 55 | } 56 | 57 | console.log(`Deleting: ${nodeModulesPath}`) 58 | try { 59 | removeFileDirectoryRecursively(nodeModulesPath); 60 | console.log(`Successfully deleted: ${nodeModulesPath}`) 61 | } catch (err) { 62 | console.log(`Error deleting ${nodeModulesPath}: ${err.message}`); 63 | } 64 | }; 65 | 66 | /// Remove all entries from the .npmignore within example/node_modules/react-native-library-name/ 67 | const removeLibraryNpmIgnorePaths = (npmIgnorePath, libraryNodeModulesPath) => { 68 | if (!fs.existsSync(npmIgnorePath)) { 69 | console.log(`No .npmignore path found at ${npmIgnorePath}. Skipping deleting content.`); 70 | return; 71 | } 72 | 73 | fs.readFileSync(npmIgnorePath, 'utf8').split(/\r?\n/).forEach(entry => { 74 | if (entry.length === 0) { 75 | return 76 | } 77 | 78 | const npmIgnoreLibraryNodeModulesEntryPath = path.resolve(libraryNodeModulesPath, entry); 79 | if (!fs.existsSync(npmIgnoreLibraryNodeModulesEntryPath)) { 80 | return; 81 | } 82 | 83 | console.log(`Deleting: ${npmIgnoreLibraryNodeModulesEntryPath}`) 84 | try { 85 | removeFileDirectoryRecursively(npmIgnoreLibraryNodeModulesEntryPath); 86 | console.log(`Successfully deleted: ${npmIgnoreLibraryNodeModulesEntryPath}`) 87 | } catch (err) { 88 | console.log(`Error deleting ${npmIgnoreLibraryNodeModulesEntryPath}: ${err.message}`); 89 | } 90 | }); 91 | }; 92 | 93 | // Main start sweeping process 94 | (() => { 95 | // Read out dir of example project 96 | const exampleDir = process.cwd(); 97 | 98 | console.log(`Starting postinstall cleanup for ${exampleDir}`); 99 | 100 | // Resolve the React Native library's path within the example's node_modules directory 101 | const libraryNodeModulesPath = process.argv.length > 2 102 | ? path.resolve(exampleDir, process.argv[2]) 103 | : path.resolve(exampleDir, 'node_modules', require('../package.json').name); 104 | 105 | console.log(`Removing unwanted artifacts for ${libraryNodeModulesPath}`); 106 | 107 | removeLibraryNodeModulesPath(libraryNodeModulesPath); 108 | 109 | const npmIgnorePath = path.resolve(__dirname, '../.npmignore'); 110 | removeLibraryNpmIgnorePaths(npmIgnorePath, libraryNodeModulesPath); 111 | })(); 112 | -------------------------------------------------------------------------------- /src/MediaClipboard.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules, NativeEventEmitter, Platform } from "react-native"; 2 | 3 | export type MediaSource = { 4 | uri: string; 5 | mimeType: string; 6 | width: number; 7 | height: number; 8 | }; 9 | 10 | export type ClipboardResponse = { 11 | urls: Array; 12 | strings: Array; 13 | hasImages: Boolean; 14 | hasURLs: Boolean; 15 | hasStrings: Boolean; 16 | }; 17 | 18 | export let MediaClipboard = NativeModules["MediaClipboard"]; 19 | 20 | if ( 21 | // @ts-ignore 22 | process.env.NODE_ENV !== "production" && 23 | !MediaClipboard && 24 | Platform.OS === "ios" 25 | ) { 26 | console.log({ MediaClipboard }); 27 | throw new Error( 28 | "Please ensure react-native-media-clipboard is linked, that you ran pod install, that you imported in your AppDelegate.m, and that you re-built the iOS app." 29 | ); 30 | } else if (!MediaClipboard && Platform.OS !== "ios") { 31 | MediaClipboard = { 32 | clipboard: { 33 | urls: [], 34 | strings: [], 35 | hasImages: false, 36 | hasURLs: false, 37 | hasStrings: false 38 | }, 39 | mediaSource: null 40 | }; 41 | } 42 | 43 | const emitter = Platform.select({ 44 | ios: new NativeEventEmitter(MediaClipboard), 45 | android: null 46 | }); 47 | 48 | export const listenToClipboardChanges = listener => 49 | emitter && emitter.addListener("MediaClipboardChange", listener); 50 | 51 | export const stopListeningToClipboardChanges = listener => 52 | emitter && emitter.removeListener("MediaClipboardChange", listener); 53 | 54 | export const listenToClipboardRemove = listener => 55 | emitter && emitter.addListener("MediaClipboardRemove", listener); 56 | 57 | export const stopListeningToClipboardRemove = listener => 58 | emitter && emitter.removeListener("MediaClipboardRemove", listener); 59 | 60 | export const getClipboardContents = (): Promise => { 61 | return new Promise((resolve, reject) => { 62 | if (Platform.OS === "android") { 63 | resolve({ 64 | urls: [], 65 | strings: [], 66 | hasImages: false, 67 | hasURLs: false, 68 | hasStrings: false 69 | }); 70 | return; 71 | } 72 | 73 | MediaClipboard.getContent((err, contents) => { 74 | if (err) { 75 | reject(err); 76 | return; 77 | } else { 78 | resolve(contents); 79 | } 80 | }); 81 | }); 82 | }; 83 | 84 | export const getClipboardMediaSource = (): Promise => { 85 | if (Platform.OS === "android") { 86 | return Promise.resolve(null); 87 | } 88 | 89 | // @ts-ignore 90 | if (typeof global.Clipboard !== "undefined") { 91 | // @ts-ignore 92 | return global.Clipboard.getMediaSource(); 93 | } else { 94 | return new Promise(resolve => 95 | MediaClipboard.clipboardMediaSource((_, content) => { 96 | resolve(content); 97 | return; 98 | }) 99 | ); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/MediaClipboardContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | MediaClipboard, 4 | ClipboardResponse, 5 | listenToClipboardChanges, 6 | stopListeningToClipboardChanges, 7 | listenToClipboardRemove, 8 | stopListeningToClipboardRemove, 9 | getClipboardMediaSource, 10 | MediaSource 11 | } from "./MediaClipboard"; 12 | 13 | export type Clipboard = { 14 | clipboard: ClipboardResponse; 15 | mediaSource: MediaSource | null; 16 | }; 17 | 18 | export const ClipboardContext = React.createContext({ 19 | clipboard: MediaClipboard.clipboard, 20 | mediaSource: MediaClipboard.mediaSource || null 21 | }); 22 | 23 | export type ClipboardProviderState = { 24 | contextValue: Clipboard; 25 | }; 26 | 27 | export type ClipboardProviderProps = { children: any }; 28 | 29 | export class ClipboardProvider extends React.Component< 30 | ClipboardProviderProps, 31 | ClipboardProviderState 32 | > { 33 | constructor(props: ClipboardProviderProps) { 34 | super(props); 35 | 36 | // @ts-ignore 37 | this.state = { 38 | contextValue: ClipboardProvider.buildContextValue( 39 | MediaClipboard.clipboard, 40 | MediaClipboard.mediaSource || null 41 | ) 42 | }; 43 | } 44 | 45 | static buildContextValue( 46 | clipboard: ClipboardResponse, 47 | mediaSource: MediaSource | null 48 | ): Clipboard { 49 | return { 50 | clipboard, 51 | mediaSource: 52 | !mediaSource || Object.keys(mediaSource).length === 0 53 | ? null 54 | : mediaSource 55 | }; 56 | } 57 | 58 | handleClipboardChange = (clipboard: ClipboardResponse) => { 59 | getClipboardMediaSource().then(mediaSource => { 60 | // @ts-ignore 61 | this.setState({ 62 | contextValue: ClipboardProvider.buildContextValue( 63 | clipboard, 64 | mediaSource || null 65 | ) 66 | }); 67 | }); 68 | }; 69 | 70 | componentDidMount() { 71 | listenToClipboardChanges(this.handleClipboardChange); 72 | listenToClipboardRemove(this.handleClipboardChange); 73 | 74 | if ( 75 | // @ts-ignore 76 | this.state.contextValue.clipboard.hasImages && 77 | // @ts-ignore 78 | !this.state.contextValue.mediaSource 79 | ) { 80 | this.updateMediaSource(); 81 | } 82 | } 83 | 84 | updateMediaSource = () => { 85 | getClipboardMediaSource().then(mediaSource => { 86 | // @ts-ignore 87 | this.setState({ 88 | contextValue: ClipboardProvider.buildContextValue( 89 | // @ts-ignore 90 | this.state.contextValue.clipboard, 91 | mediaSource 92 | ) 93 | }); 94 | }); 95 | }; 96 | 97 | componentWillUnmount() { 98 | stopListeningToClipboardChanges(this.handleClipboardChange); 99 | stopListeningToClipboardRemove(this.handleClipboardChange); 100 | } 101 | 102 | render() { 103 | // @ts-ignore 104 | const { children } = this.props; 105 | return ( 106 | // @ts-ignore 107 | 108 | {children} 109 | 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Clipboard, 3 | ClipboardContext, 4 | ClipboardProvider 5 | } from "./MediaClipboardContext"; 6 | 7 | export { 8 | MediaSource, 9 | getClipboardMediaSource, 10 | listenToClipboardChanges, 11 | stopListeningToClipboardChanges, 12 | listenToClipboardRemove, 13 | stopListeningToClipboardRemove, 14 | getClipboardContents, 15 | ClipboardResponse 16 | } from "./MediaClipboard"; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": true, 4 | "allowUnusedLabels": true, 5 | "alwaysStrict": false, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "lib": ["esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": false, 15 | "noImplicitThis": false, 16 | "noImplicitUseStrict": false, 17 | "noStrictGenericChecks": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "strict": false, 23 | "target": "esnext" 24 | }, 25 | "exclude": ["typings/**/*", "lib/**/*", "templates/**/*"] 26 | } 27 | --------------------------------------------------------------------------------