fix: Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor (#2336)
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
package com.standardnotes;
|
||||||
|
|
||||||
|
import com.reactnativecommunity.webview.RNCWebViewManager;
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
|
import android.view.inputmethod.InputConnectionWrapper;
|
||||||
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
|
@ReactModule(name = CustomWebViewManager.REACT_CLASS)
|
||||||
|
public class CustomWebViewManager extends RNCWebViewManager {
|
||||||
|
/* This name must match what we’re referring to in JS */
|
||||||
|
protected static final String REACT_CLASS = "CustomWebView";
|
||||||
|
|
||||||
|
protected static class CustomWebViewClient extends RNCWebViewClient {}
|
||||||
|
|
||||||
|
protected static class CustomWebView extends RNCWebView {
|
||||||
|
public CustomWebView(ThemedReactContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||||
|
InputConnection con = super.onCreateInputConnection(outAttrs);
|
||||||
|
if (con == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new CustomInputConnection(con, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomInputConnection extends InputConnectionWrapper {
|
||||||
|
public CustomInputConnection(InputConnection target, boolean mutable) {
|
||||||
|
super(target, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sendKeyEvent(KeyEvent event) {
|
||||||
|
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
||||||
|
// Un-comment if you wish to cancel the backspace:
|
||||||
|
// return false;
|
||||||
|
}
|
||||||
|
return super.sendKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||||
|
// magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace
|
||||||
|
if (beforeLength == 1 && afterLength == 0) {
|
||||||
|
// backspace
|
||||||
|
return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||||
|
&& sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
|
||||||
|
}
|
||||||
|
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
|
||||||
|
return new CustomWebView(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return REACT_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||||
|
view.setWebViewClient(new CustomWebViewClient());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.standardnotes;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
|
public class CustomWebViewPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Arrays.<ViewManager>asList(new CustomWebViewManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ public class MainApplication extends Application implements ReactApplication {
|
|||||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
|
|
||||||
packages.add(new Fido2ApiPackage());
|
packages.add(new Fido2ApiPackage());
|
||||||
|
packages.add(new CustomWebViewPackage());
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Button, Keyboard, Platform, Text, View } from 'react-native'
|
import { Button, Keyboard, Platform, requireNativeComponent, Text, View } from 'react-native'
|
||||||
import VersionInfo from 'react-native-version-info'
|
import VersionInfo from 'react-native-version-info'
|
||||||
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
||||||
import { OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
import { NativeWebViewAndroid, OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
||||||
import { AndroidBackHandlerService } from './AndroidBackHandlerService'
|
import { AndroidBackHandlerService } from './AndroidBackHandlerService'
|
||||||
import { AppStateObserverService } from './AppStateObserverService'
|
import { AppStateObserverService } from './AppStateObserverService'
|
||||||
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
||||||
import { MobileDevice, MobileDeviceEvent } from './Lib/MobileDevice'
|
import { MobileDevice, MobileDeviceEvent } from './Lib/MobileDevice'
|
||||||
import { IsDev } from './Lib/Utils'
|
import { IsDev } from './Lib/Utils'
|
||||||
|
|
||||||
|
const CustomWebView: NativeWebViewAndroid = requireNativeComponent('CustomWebView')
|
||||||
|
|
||||||
const LoggingEnabled = IsDev
|
const LoggingEnabled = IsDev
|
||||||
|
|
||||||
export const MobileWebAppContainer = () => {
|
export const MobileWebAppContainer = () => {
|
||||||
@@ -354,6 +356,12 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
* This is needed to prevent the keyboard from pushing the webview up and down when it appears and disappears.
|
* This is needed to prevent the keyboard from pushing the webview up and down when it appears and disappears.
|
||||||
*/
|
*/
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
|
overScrollMode="never"
|
||||||
|
nativeConfig={Platform.select({
|
||||||
|
android: {
|
||||||
|
component: CustomWebView,
|
||||||
|
},
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import useModal from '../../Lexical/Hooks/useModal'
|
|||||||
import { InsertTableDialog } from '../../Plugins/TablePlugin'
|
import { InsertTableDialog } from '../../Plugins/TablePlugin'
|
||||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||||
import { $getSelection, $isRangeSelection, DELETE_CHARACTER_COMMAND, FORMAT_TEXT_COMMAND } from 'lexical'
|
import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
||||||
@@ -23,14 +23,11 @@ import { GetPasswordBlock } from '../Blocks/Password'
|
|||||||
import { GetQuoteBlock } from '../Blocks/Quote'
|
import { GetQuoteBlock } from '../Blocks/Quote'
|
||||||
import { GetTableBlock } from '../Blocks/Table'
|
import { GetTableBlock } from '../Blocks/Table'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import { classNames, Platform } from '@standardnotes/snjs'
|
import { classNames } from '@standardnotes/snjs'
|
||||||
import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
||||||
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||||
import { SKAlert } from '@standardnotes/styles'
|
|
||||||
|
|
||||||
const DontShowSuperAndroidBackspaceAlertKey = 'dontShowSuperAndroidBackspaceAlert'
|
|
||||||
|
|
||||||
const MobileToolbarPlugin = () => {
|
const MobileToolbarPlugin = () => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -40,7 +37,6 @@ const MobileToolbarPlugin = () => {
|
|||||||
const [isInEditor, setIsInEditor] = useState(false)
|
const [isInEditor, setIsInEditor] = useState(false)
|
||||||
const [isInToolbar, setIsInToolbar] = useState(false)
|
const [isInToolbar, setIsInToolbar] = useState(false)
|
||||||
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||||
const isAndroid = application.platform === Platform.Android
|
|
||||||
|
|
||||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||||
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
@@ -204,38 +200,6 @@ const MobileToolbarPlugin = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAndroid) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dontShowAgain = application.getValue(DontShowSuperAndroidBackspaceAlertKey)
|
|
||||||
|
|
||||||
if (dontShowAgain) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const alert = new SKAlert({
|
|
||||||
title: 'Android backspace issue',
|
|
||||||
text: 'There is a known issue with Super on Android where pressing the backspace will also delete the character after the cursor. We are working on a fix for this. Please use the backspace button in the toolbar as a workaround.',
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'OK',
|
|
||||||
style: 'default',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Don't show again",
|
|
||||||
style: 'default',
|
|
||||||
action: () => {
|
|
||||||
application.setValue(DontShowSuperAndroidBackspaceAlertKey, true)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
alert.present()
|
|
||||||
}, [application, isAndroid])
|
|
||||||
|
|
||||||
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
|
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
|
||||||
if (!isMobile || !isFocusInEditorOrToolbar) {
|
if (!isMobile || !isFocusInEditorOrToolbar) {
|
||||||
return null
|
return null
|
||||||
@@ -269,20 +233,6 @@ const MobileToolbarPlugin = () => {
|
|||||||
>
|
>
|
||||||
<Icon type="keyboard-close" size="medium" />
|
<Icon type="keyboard-close" size="medium" />
|
||||||
</button>
|
</button>
|
||||||
{isAndroid && (
|
|
||||||
<button
|
|
||||||
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border py-3 px-3"
|
|
||||||
aria-label="Backspace"
|
|
||||||
ref={backspaceButtonRef}
|
|
||||||
onClick={() => {
|
|
||||||
editor.update(() => {
|
|
||||||
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon type="backspace" size="medium" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user