diff --git a/.eslintrc.yaml b/.eslintrc.yaml index ee0fe1d5d7c..3629246fb34 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -225,7 +225,11 @@ rules: - devDependencies: ['**/__tests__/**/*.js', tools/**] no-restricted-imports: - error - - patterns: + - paths: + - name: 'react-native' + importNames: ['Clipboard'] + message: 'Use Clipboard from @react-native-clipboard/clipboard instead.' + patterns: - group: ['**/__tests__/**'] - group: ['/react-redux'] message: 'Use our own src/react-redux.js instead.' diff --git a/.flowconfig b/.flowconfig index 09f49527fc0..5dcaf3c9dc1 100644 --- a/.flowconfig +++ b/.flowconfig @@ -120,7 +120,7 @@ module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' # This lets us write .js.flow files instead of libdefs. # Add more libraries as needed to this pattern with `\|`: `foo\|bar\|…`. -module.name_mapper='^\(sqlite3\|@react-navigation/bottom-tabs\|@react-navigation/drawer\|@react-navigation/material-top-tabs\|@react-navigation/native\|@react-navigation/stack\)$' -> '/types/\0' +module.name_mapper='^\(sqlite3\|@react-navigation/bottom-tabs\|@react-navigation/drawer\|@react-navigation/material-top-tabs\|@react-navigation/native\|@react-navigation/stack\|@react-native-clipboard/clipboard\)$' -> '/types/\0' suppress_type=$FlowIssue suppress_type=$FlowFixMe diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6fadfffaee5..e3b8365a74b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -360,6 +360,8 @@ PODS: - React-Core - RNCAsyncStorage (1.16.1): - React-Core + - RNCClipboard (1.8.5): + - React-Core - RNCMaskedView (0.1.11): - React - RNCPushNotificationIOS (1.10.1): @@ -480,6 +482,7 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - rn-fetch-blob (from `../node_modules/rn-fetch-blob`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -603,6 +606,8 @@ EXTERNAL SOURCES: :path: "../node_modules/rn-fetch-blob" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" RNCMaskedView: :path: "../node_modules/@react-native-community/masked-view" RNCPushNotificationIOS: @@ -680,6 +685,7 @@ SPEC CHECKSUMS: ReactCommon: 8fea6422328e2fc093e25c9fac67adbcf0f04fb4 rn-fetch-blob: f525a73a78df9ed5d35e67ea65e79d53c15255bc RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7 + RNCClipboard: cc054ad1e8a33d2a74cd13e565588b4ca928d8fd RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNCPushNotificationIOS: 87b8d16d3ede4532745e05b03c42cff33a36cc45 RNDeviceInfo: 4944cf8787b9c5bffaf301fda68cc1a2ec003341 diff --git a/jest/jestSetup.js b/jest/jestSetup.js index e5589b0adb6..1ccf6bcba4b 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -5,6 +5,8 @@ import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctio import { URL, URLSearchParams } from 'react-native-url-polyfill'; // $FlowIgnore[untyped-import] - this is not anywhere near critical import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'; +// $FlowIgnore[untyped-import] - this is not anywhere near critical +import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock'; import { assertUsingModernFakeTimers } from '../src/__tests__/lib/fakeTimers'; @@ -102,6 +104,10 @@ jest.mock('react-native-reanimated', () => { jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); +// As instructed at +// https://github.com/react-native-clipboard/clipboard/tree/v1.9.0#mocking-clipboard +jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); + // Without this, we get lots of these errors on importing the module: // `Invariant Violation: Native module cannot be null.` jest.mock('@react-native-community/push-notification-ios', () => ({ diff --git a/package.json b/package.json index 144c68c9788..4b50912689e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@expo/react-native-action-sheet": "^3.8.0", "@react-native-async-storage/async-storage": "^1.13.0", + "@react-native-clipboard/clipboard": "^1.8.5", "@react-native-community/cameraroll": "chrisbobbe/react-native-cameraroll#17fa5d8d2", "@react-native-community/masked-view": "^0.1.10", "@react-native-community/netinfo": "6.0.0", diff --git a/src/RootErrorBoundary.js b/src/RootErrorBoundary.js index e35ff2c40eb..993a8ba28ae 100644 --- a/src/RootErrorBoundary.js +++ b/src/RootErrorBoundary.js @@ -1,7 +1,8 @@ /* @flow strict-local */ import React from 'react'; import type { Node } from 'react'; -import { View, Text, Clipboard, TextInput, ScrollView, Button, Platform } from 'react-native'; +import { View, Text, TextInput, ScrollView, Button, Platform } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; import Toast from 'react-native-simple-toast'; // $FlowFixMe[untyped-import] import isEqual from 'lodash.isequal'; diff --git a/src/action-sheets/index.js b/src/action-sheets/index.js index e0199d8d57a..c662cf2bfdb 100644 --- a/src/action-sheets/index.js +++ b/src/action-sheets/index.js @@ -1,5 +1,6 @@ /* @flow strict-local */ -import { Clipboard, Share, Alert } from 'react-native'; +import { Share, Alert } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; import invariant from 'invariant'; import * as resolved_topic from '@zulip/shared/js/resolved_topic'; diff --git a/src/webview/handleOutboundEvents.js b/src/webview/handleOutboundEvents.js index d986d5e9d72..57b95097f28 100644 --- a/src/webview/handleOutboundEvents.js +++ b/src/webview/handleOutboundEvents.js @@ -1,5 +1,6 @@ /* @flow strict-local */ -import { Clipboard, Alert } from 'react-native'; +import { Alert } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; import * as NavigationService from '../nav/NavigationService'; import * as api from '../api'; diff --git a/types/@react-native-clipboard/clipboard.js.flow b/types/@react-native-clipboard/clipboard.js.flow new file mode 100644 index 00000000000..2abec4bcd4b --- /dev/null +++ b/types/@react-native-clipboard/clipboard.js.flow @@ -0,0 +1,154 @@ +/** + * Flowtype definitions for Clipboard + * Generated by Flowgen from a Typescript Definition + * Flowgen v1.11.0 + * + * @flow strict-local + */ + +import type EmitterSubscription from 'react-native/Libraries/vendor/emitter/_EmitterSubscription'; + +/** + * `Clipboard` gives you an interface for setting and getting content from Clipboard on both iOS and Android + */ +declare var Clipboard: { + /** + * Get content of string type, this method returns a `Promise`, so you can use following code to get clipboard content + * ```javascript + * async _getContent() { + * var content = await Clipboard.getString(); + * } + * ``` + */ + getString(): Promise, + + /** + * Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content + * ```javascript + * async _getContent() { + * var content = await Clipboard.getImagePNG(); + * } + * ``` + */ + getImagePNG(): Promise, + + /** + * Get clipboard image as JPG in base64, this method returns a `Promise`, so you can use following code to get clipboard content + * ```javascript + * async _getContent() { + * var content = await Clipboard.getImageJPG(); + * } + * ``` + */ + getImageJPG(): Promise, + + /** + * Set content of base64 image type. You can use following code to set clipboard content + * ```javascript + * _setContent() { + * Clipboard.setImage(...); + * } + * + * iOS only + * ``` + * @param the content to be stored in the clipboard. + */ + setImage(content: string): void, + getImage(): Promise, + + /** + * Set content of string type. You can use following code to set clipboard content + * ```javascript + * _setContent() { + * Clipboard.setString('hello world'); + * } + * ``` + * @param the content to be stored in the clipboard. + */ + setString(content: string): void, + + /** + * Returns whether the clipboard has content or is empty. + * This method returns a `Promise`, so you can use following code to get clipboard content + * ```javascript + * async _hasContent() { + * var hasContent = await Clipboard.hasString(); + * } + * ``` + */ + hasString(): $FlowFixMe, // `any` in TypeScript upstream :( + + /** + * Returns whether the clipboard has an image or is empty. + * This method returns a `Promise`, so you can use following code to check clipboard content + * ```javascript + * async _hasContent() { + * var hasContent = await Clipboard.hasImage(); + * } + * ``` + */ + hasImage(): $FlowFixMe, // `any` in TypeScript upstream :( + + /** + * (IOS Only) + * Returns whether the clipboard has a URL content. Can check + * if there is a URL content in clipboard without triggering PasteBoard notification for iOS 14+ + * This method returns a `Promise`, so you can use following code to check for url content in clipboard. + * ```javascript + * async _hasURL() { + * var hasURL = await Clipboard.hasURL(); + * } + * ``` + */ + hasURL(): $FlowFixMe, // `any` in TypeScript upstream :( + + /** + * (IOS 14+ Only) + * Returns whether the clipboard has a Number(UIPasteboardDetectionPatternNumber) content. Can check + * if there is a Number content in clipboard without triggering PasteBoard notification for iOS 14+ + * This method returns a `Promise`, so you can use following code to check for Number content in clipboard. + * ```javascript + * async _hasNumber() { + * var hasNumber = await Clipboard.hasNumber(); + * } + * ``` + */ + hasNumber(): $FlowFixMe, // `any` in TypeScript upstream :( + + /** + * (IOS 14+ Only) + * Returns whether the clipboard has a WebURL(UIPasteboardDetectionPatternProbableWebURL) content. Can check + * if there is a WebURL content in clipboard without triggering PasteBoard notification for iOS 14+ + * This method returns a `Promise`, so you can use following code to check for WebURL content in clipboard. + * ```javascript + * async _hasWebURL() { + * var hasWebURL = await Clipboard.hasWebURL(); + * } + * ``` + */ + hasWebURL(): $FlowFixMe, // `any` in TypeScript upstream :( + + /** + * (iOS and Android Only) + * Adds a listener to get notifications when the clipboard has changed. + * If this is the first listener, turns on clipboard notifications on the native side. + * It returns EmitterSubscription where you can call "remove" to remove listener + * ```javascript + * const listener = () => console.log("changed!"); + * Clipboard.addListener(listener); + * ``` + */ + addListener(callback: () => void): EmitterSubscription, + + /** + * (iOS and Android Only) + * Removes all previously registered listeners and turns off notifications on the native side. + * ```javascript + * Clipboard.removeAllListeners(); + * ``` + */ + removeAllListeners(): void, + ... +}; + +export default Clipboard; diff --git a/yarn.lock b/yarn.lock index f14e38feedd..be77dee529d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,6 +1795,11 @@ dependencies: merge-options "^3.0.4" +"@react-native-clipboard/clipboard@^1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.8.5.tgz#b11276e38ef288b0fd70c0a38506e2deecc5fa5a" + integrity sha512-o2RPDwP9JMnLece1Qq6a3Fsz/VxfA9auLckkGOor7WcI82DWaWiJ6Uiyu7H1xpaUyqWc+ypVKRX680GYS36HjA== + "@react-native-community/cameraroll@chrisbobbe/react-native-cameraroll#17fa5d8d2": version "4.0.4" resolved "https://codeload.github.com/chrisbobbe/react-native-cameraroll/tar.gz/17fa5d8d2f4e00ec78304070a0b91292e884b7f5"