Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

realm input screen: Add "Use copied URL" button #5328

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -680,6 +685,7 @@ SPEC CHECKSUMS:
ReactCommon: 8fea6422328e2fc093e25c9fac67adbcf0f04fb4
rn-fetch-blob: f525a73a78df9ed5d35e67ea65e79d53c15255bc
RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7
RNCClipboard: cc054ad1e8a33d2a74cd13e565588b4ca928d8fd
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
RNCPushNotificationIOS: 87b8d16d3ede4532745e05b03c42cff33a36cc45
RNDeviceInfo: 4944cf8787b9c5bffaf301fda68cc1a2ec003341
Expand Down
6 changes: 6 additions & 0 deletions jest/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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', () => ({
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 74 additions & 0 deletions src/@react-native-clipboard/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Helpers for @react-native-clipboard/clipboard
*
* @flow strict-local
*/

import { useState, useCallback, useEffect } from 'react';
import { AppState, Platform } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';

import * as logging from '../utils/logging';
import { tryParseUrl } from '../utils/url';

/**
* A Hook for the current value of Clipboard.hasURL, when known.
*
* https://github.com/react-native-clipboard/clipboard#hasurl
*
* With a hack to simulate Clipboard.hasURL on iOS <14, and on Android.
*
* Returns the payload of the most recently settled Clipboard.hasURL()
* Promise; otherwise `null` if that Promise rejected, or if no such Promise
* has settled.
*
* Re-queries when clipboard value changes, and when app state changes to
* "active".
*
* Subject to subtle races. Don't use for anything critical, and do a
* sanity-check in clipboard reads informed by the result of this (e.g.,
* check the retrieved string with `tryParseUrl`).
*/
export function useClipboardHasURL(): boolean | null {
const [result, setResult] = useState<boolean | null>(null);

const getAndSetResult = useCallback(async () => {
try {
// TODO(ios-14): Simplify conditional and jsdoc.
if (Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 14) {
setResult(await Clipboard.hasURL());
} else {
// Hack: Simulate Clipboard.hasURL
setResult(!!tryParseUrl(await Clipboard.getString()));
}
} catch (e) {
logging.error(e);
setResult(null);
}
}, []);

useEffect(() => {
getAndSetResult();

const clipboardListener = Clipboard.addListener(() => {
getAndSetResult();
});

const appStateChangeListener = AppState.addEventListener('change', s => {
if (s === 'active') {
getAndSetResult();
}
});

return () => {
clipboardListener.remove();
AppState.removeEventListener('change', appStateChangeListener);
};
}, [getAndSetResult]);

return result;
}

// We probably don't want a useClipboardHasWebURL. The implementation of
// Clipboard.hasWebURL on iOS is such that it matches when the copied string
// *has* a URL, not when it *is* a URL.
3 changes: 2 additions & 1 deletion src/RootErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/action-sheets/index.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
19 changes: 5 additions & 14 deletions src/common/SmartUrlInput.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow strict-local */
import React, { useState, useRef, useCallback, useContext } from 'react';
import React, { useRef, useCallback, useContext } from 'react';
import type { Node } from 'react';
import { TextInput, View } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
Expand Down Expand Up @@ -28,21 +28,20 @@ type Props = $ReadOnly<{|

style?: ViewStyleProp,
onChangeText: (value: string) => void,
onSubmitEditing: () => Promise<void>,
value: string,
onSubmitEditing: () => void,
enablesReturnKeyAutomatically: boolean,
|}>;

export default function SmartUrlInput(props: Props): Node {
const { style, onChangeText, onSubmitEditing, enablesReturnKeyAutomatically } = props;
const { style, onChangeText, value, onSubmitEditing, enablesReturnKeyAutomatically } = props;

// We should replace the fixme with
// `React$ElementRef<typeof TextInput>` when we can. Currently, that
// would make `.current` be `any(implicit)`, which we don't want;
// this is probably down to bugs in Flow's special support for React.
const textInputRef = useRef<$FlowFixMe>();

const [value, setValue] = useState<string>('');

const themeContext = useContext(ThemeContext);

// When the route is focused in the navigation, focus the input.
Expand All @@ -65,14 +64,6 @@ export default function SmartUrlInput(props: Props): Node {
}, []),
);

const handleChange = useCallback(
(_value: string) => {
setValue(_value);
onChangeText(_value);
},
[onChangeText],
);

return (
<View style={[styles.wrapper, style]}>
<TextInput
Expand All @@ -84,7 +75,7 @@ export default function SmartUrlInput(props: Props): Node {
autoCorrect={false}
autoCapitalize="none"
returnKeyType="go"
onChangeText={handleChange}
onChangeText={onChangeText}
blurOnSubmit={false}
keyboardType="url"
underlineColorAndroid="transparent"
Expand Down
Loading