Mobile: Upgrade to React Native 0.79 (#12337)

This commit is contained in:
Henry Heino 2025-06-11 01:35:51 -07:00 committed by GitHub
parent caba91fdf6
commit 42a156c2bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1693 additions and 1718 deletions

View File

@ -1,25 +0,0 @@
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
index 8a719ca35af1cc3a4192c5c5f8258fd4f7fea990..5f8831f81cd164a4f627423427ead92fa286b115 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
@@ -37,7 +37,7 @@ import com.facebook.react.uimanager.common.ViewUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -149,7 +149,10 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
}
private class ConcurrentOperationQueue {
- private final Queue<UIThreadOperation> mQueue = new ConcurrentLinkedQueue<>();
+ // Patch: Use LinkedBlockingQueue instead of ConcurrentLinkedQueue.
+ // In some versions of Android, ConcurrentLinkedQueue is known to drop
+ // items, causing crashing. See https://github.com/laurent22/joplin/issues/8425
+ private final Queue<UIThreadOperation> mQueue = new LinkedBlockingQueue<>();
@Nullable private UIThreadOperation mPeekedOperation = null;
@AnyThread

View File

@ -0,0 +1,205 @@
# This patch fixes two issues:
# - Updates RCTDeviceInfo.m to match https://github.com/facebook/react-native/commit/0b8db7e5e814cfbf9974cc5b6ceb64e8006d8a3c.
# This fixes an issue in which useWindowDimensions returns incorrect
# values in landscape mode in iOS.
# This should be fixed in React Native 0.80. See https://github.com/facebook/react-native/issues/51086.
# - Updates NativeAnimatedModule.java to work around an Android 12-specific crash.
diff --git a/React/CoreModules/RCTDeviceInfo.mm b/React/CoreModules/RCTDeviceInfo.mm
index 6b4fcef852252e8d4ac2aceb12175fdfafb4def7..8ceab21e8653d429876d10e2d12ed1342780ad7d 100644
--- a/React/CoreModules/RCTDeviceInfo.mm
+++ b/React/CoreModules/RCTDeviceInfo.mm
@@ -14,9 +14,7 @@
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
#import <React/RCTInvalidating.h>
-#import <React/RCTKeyWindowValuesProxy.h>
#import <React/RCTUtils.h>
-#import <React/RCTWindowSafeAreaProxy.h>
#import <atomic>
#import "CoreModulesPlugins.h"
@@ -31,8 +29,13 @@ using namespace facebook::react;
NSDictionary *_currentInterfaceDimensions;
BOOL _isFullscreen;
std::atomic<BOOL> _invalidated;
+ NSDictionary *_constants;
+
+ __weak UIWindow *_applicationWindow;
}
+static NSString *const kFrameKeyPath = @"frame";
+
@synthesize moduleRegistry = _moduleRegistry;
RCT_EXPORT_MODULE()
@@ -40,14 +43,26 @@ RCT_EXPORT_MODULE()
- (instancetype)init
{
if (self = [super init]) {
- [[RCTKeyWindowValuesProxy sharedInstance] startObservingWindowSizeIfNecessary];
+ _applicationWindow = RCTKeyWindow();
+ [_applicationWindow addObserver:self forKeyPath:kFrameKeyPath options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary *)change
+ context:(void *)context
+{
+ if ([keyPath isEqualToString:kFrameKeyPath]) {
+ [self interfaceFrameDidChange];
+ [[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
+ }
+}
+
+ (BOOL)requiresMainQueueSetup
{
- return NO;
+ return YES;
}
- (dispatch_queue_t)methodQueue
@@ -81,7 +96,7 @@ RCT_EXPORT_MODULE()
#if TARGET_OS_IOS
- _currentInterfaceOrientation = [RCTKeyWindowValuesProxy sharedInstance].currentInterfaceOrientation;
+ _currentInterfaceOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfaceFrameDidChange)
@@ -98,6 +113,15 @@ RCT_EXPORT_MODULE()
selector:@selector(invalidate)
name:RCTBridgeWillInvalidateModulesNotification
object:nil];
+
+ _constants = @{
+ @"Dimensions" : [self _exportedDimensions],
+ // Note:
+ // This prop is deprecated and will be removed in a future release.
+ // Please use this only for a quick and temporary solution.
+ // Use <SafeAreaView> instead.
+ @"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
+ };
}
- (void)invalidate
@@ -120,6 +144,8 @@ RCT_EXPORT_MODULE()
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTBridgeWillInvalidateModulesNotification object:nil];
+ [_applicationWindow removeObserver:self forKeyPath:kFrameKeyPath];
+
#if TARGET_OS_IOS
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
#endif
@@ -132,8 +158,13 @@ static BOOL RCTIsIPhoneNotched()
#if TARGET_OS_IOS
dispatch_once(&onceToken, ^{
+ RCTAssertMainQueue();
+
// 20pt is the top safeArea value in non-notched devices
- isIPhoneNotched = [RCTWindowSafeAreaProxy sharedInstance].currentSafeAreaInsets.top > 20;
+ UIWindow *keyWindow = RCTKeyWindow();
+ if (keyWindow) {
+ isIPhoneNotched = keyWindow.safeAreaInsets.top > 20;
+ }
});
#endif
@@ -142,11 +173,13 @@ static BOOL RCTIsIPhoneNotched()
static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
{
+ RCTAssertMainQueue();
UIScreen *mainScreen = UIScreen.mainScreen;
CGSize screenSize = mainScreen.bounds.size;
+ UIView *mainWindow = RCTKeyWindow();
// We fallback to screen size if a key window is not found.
- CGSize windowSize = [RCTKeyWindowValuesProxy sharedInstance].windowSize;
+ CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize;
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
@"width" : @(windowSize.width),
@@ -170,7 +203,10 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
RCTAssert(_moduleRegistry, @"Failed to get exported dimensions: RCTModuleRegistry is nil");
RCTAccessibilityManager *accessibilityManager =
(RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"];
- RCTAssert(accessibilityManager, @"Failed to get exported dimensions: AccessibilityManager is nil");
+ // TOOD(T225745315): For some reason, accessibilityManager is nil in some cases.
+ // We default the fontScale to 1.0 in this case. This should be okay: if we assume
+ // that accessibilityManager will eventually become available, js will eventually
+ // be updated with the correct fontScale.
CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0;
return RCTExportedDimensions(fontScale);
}
@@ -182,14 +218,7 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
- (NSDictionary<NSString *, id> *)getConstants
{
- return @{
- @"Dimensions" : [self _exportedDimensions],
- // Note:
- // This prop is deprecated and will be removed in a future release.
- // Please use this only for a quick and temporary solution.
- // Use <SafeAreaView> instead.
- @"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
- };
+ return _constants;
}
- (void)didReceiveNewContentSizeMultiplier
@@ -209,10 +238,11 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
- (void)interfaceOrientationDidChange
{
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
- UIWindow *keyWindow = RCTKeyWindow();
- UIInterfaceOrientation nextOrientation = keyWindow.windowScene.interfaceOrientation;
+ UIApplication *application = RCTSharedApplication();
+ UIInterfaceOrientation nextOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
- BOOL isRunningInFullScreen = CGRectEqualToRect(keyWindow.frame, keyWindow.screen.bounds);
+ BOOL isRunningInFullScreen =
+ CGRectEqualToRect(application.delegate.window.frame, application.delegate.window.screen.bounds);
// We are catching here two situations for multitasking view:
// a) The app is in Split View and the container gets resized -> !isRunningInFullScreen
// b) The app changes to/from fullscreen example: App runs in slide over mode and goes into fullscreen->
@@ -276,3 +306,4 @@ Class RCTDeviceInfoCls(void)
{
return RCTDeviceInfo.class;
}
+
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
index cf14e51cf5f561b84f1b6ace8410fc77d626758e..abc8c64adf26fbf73429aee7fd4f76877e98849a 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
@@ -42,6 +42,7 @@ import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -155,8 +156,15 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
}
private class ConcurrentOperationQueue {
- private final Queue<UIThreadOperation> mQueue = new ConcurrentLinkedQueue<>();
- @Nullable private UIThreadOperation mPeekedOperation = null;
+ // Patch: Use LinkedBlockingQueue instead of ConcurrentLinkedQueue.
+ // In some versions of Android, ConcurrentLinkedQueue is known to drop
+ // items, causing crashing. See https://github.com/laurent22/joplin/issues/8425
+ private final Queue<UIThreadOperation> mQueue = (
+ // The issue exists for Android 12, which corresponds to API levels 31 and 32.
+ Build.VERSION.SDK_INT == 31 || Build.VERSION.SDK_INT == 32
+ ) ? new LinkedBlockingQueue<>() : new ConcurrentLinkedQueue<>();
+
+ @Nullable private UIThreadOperation mPeekedOperation = null;
@AnyThread
boolean isEmpty() {

View File

@ -104,13 +104,13 @@
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"chokidar@^2.0.0": "3.5.3",
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
"app-builder-lib@26.0.0-alpha.7": "patch:app-builder-lib@npm%3A26.0.0-alpha.7#./.yarn/patches/app-builder-lib-npm-26.0.0-alpha.7-e1b3dca119.patch",
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
"react-native-popup-menu@0.17.0": "patch:react-native-popup-menu@npm%3A0.17.0#./.yarn/patches/react-native-popup-menu-npm-0.17.0-8b745d88dd.patch",
"react-native@0.79.2": "patch:react-native@npm%3A0.79.2#./.yarn/patches/react-native-npm-0.79.2-9db13eddfe.patch",
"pdfjs-dist@2.16.105": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",

View File

@ -6,7 +6,7 @@ buildscript {
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 34
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,10 +1,42 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
pluginManagement {
def reactNativeGradlePlugin = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim()
).getParentFile().absolutePath
includeBuild(reactNativeGradlePlugin)
def expoPluginsPath = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim(),
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
}
plugins {
id("com.facebook.react.settings")
id("expo-autolinking-settings")
}
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
expoAutolinking.useExpoModules()
rootProject.name = 'Joplin'
expoAutolinking.useExpoVersionCatalog()
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()
includeBuild(expoAutolinking.reactNativeGradlePlugin)

View File

@ -58,7 +58,7 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
// iOS issue workaround: Since upgrading to Expo SDK 52, closing and reopening the camera on iOS
// never emits onCameraReady. As a workaround, call .resumePreview and wait for it to resolve,
// rather than relying on the CameraView's onCameraReady prop.
if (Platform.OS === 'ios') {
if (Platform.OS === 'ios' && camera) {
// Work around an issue on iOS where the onCameraReady callback is never called.
// Instead, wait for the preview to start using resumePreview:
await camera.resumePreview();
@ -67,7 +67,7 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
}
}, [camera, props.onCameraReady]);
return <CameraView
return hasPermission?.granted ? <CameraView
ref={setCamera}
style={props.style}
facing={props.cameraType === CameraDirection.Front ? 'front' : 'back'}
@ -77,7 +77,7 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
animateShutter={false}
barcodeScannerSettings={barcodeScannerSettings}
onBarcodeScanned={props.codeScanner.onBarcodeScanned}
/>;
/> : null;
};
export default forwardRef(Camera);

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import CameraView from './CameraView';
import { CameraResult } from './types';
import { fireEvent, render, screen } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import TestProviderStack from '../testing/TestProviderStack';

View File

@ -3,7 +3,6 @@ import { Text } from 'react-native';
import { describe, it, expect, jest } from '@jest/globals';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native';
import Dropdown, { DropdownListItem } from './Dropdown';

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { Store } from 'redux';
import { AppState } from '../../utils/types';
@ -46,11 +45,19 @@ const toggleSettingsItem = async (props: ToggleSettingItemProps) => {
const itemCheckbox = await screen.findByRole('checkbox', { name: props.name });
expect(itemCheckbox).toBeVisible();
expect(itemCheckbox).toHaveAccessibilityState({ checked: initialChecked });
if (initialChecked) {
expect(itemCheckbox).toBeChecked();
} else {
expect(itemCheckbox).not.toBeChecked();
}
fireEvent.press(itemCheckbox);
await waitFor(() => {
expect(itemCheckbox).toHaveAccessibilityState({ checked: finalChecked });
if (finalChecked) {
expect(itemCheckbox).toBeChecked();
} else {
expect(itemCheckbox).not.toBeChecked();
}
});
};

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { describe, it, beforeEach } from '@jest/globals';
import { render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import NoteBodyViewer from './NoteBodyViewer';

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { describe, it, expect, beforeEach } from '@jest/globals';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native';
import NoteEditor from './NoteEditor';
import Setting from '@joplin/lib/models/Setting';

View File

@ -3,7 +3,6 @@ import { WarningBannerComponent } from './WarningBanner';
import Setting from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import { render, screen, userEvent } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer';
import makeShareInvitation from '@joplin/lib/testing/share/makeMockShareInvitation';

View File

@ -3,7 +3,6 @@ import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
import { expect, describe, beforeEach, test, jest } from '@jest/globals';
import '@testing-library/jest-native/extend-expect';
import { createNTestNotes, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import Folder from '@joplin/lib/models/Folder';
import configScreenStyles from '../configScreenStyles';

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
import '@testing-library/react-native/extend-expect';
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
import pluginServiceSetup from './testUtils/pluginServiceSetup';

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import { render, screen, userEvent, waitFor } from '@testing-library/react-native';
import '@testing-library/react-native/extend-expect';
import pluginServiceSetup from './testUtils/pluginServiceSetup';
import createMockReduxStore from '../../../../utils/testing/createMockReduxStore';

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { describe, it, beforeEach } from '@jest/globals';
import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import NoteScreen from './Note';
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
@ -113,7 +112,7 @@ const openNoteActionsMenu = async () => {
// Wrap in act(...) -- this tells the test library that component state is intended to update (prevents
// warnings).
await act(async () => {
await waitFor(async () => {
await runWithFakeTimers(async () => {
await userEvent.press(actionMenuButton);
});
@ -156,10 +155,7 @@ describe('screens/Note', () => {
// In order for note changes to be saved, note-screen-shared requires
// that at least one folder exist.
await Folder.save({ title: 'test', parent_id: '' });
});
afterEach(() => {
screen.unmount();
jest.useRealTimers();
});
it('should show the currently selected note', async () => {
@ -210,27 +206,27 @@ describe('screens/Note', () => {
const noteId = await openNewNote({ title: 'Unchanged title', body: defaultBody });
const noteScreen = render(<WrappedNoteScreen />);
await act(async () => await runWithFakeTimers(async () => {
await openEditor();
const editor = await getNoteEditorControl();
await openEditor();
const editor = await getNoteEditorControl();
await act(async () => {
editor.select(defaultBody.length, defaultBody.length);
editor.insertText(' Testing!!!');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!!' });
expect(editor.editor.state.doc.toString()).toBe('Change me! Testing!!!');
});
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!!' });
await act(async () => {
editor.insertText(' This is a test.');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test.' });
// should also save changes made shortly before unmounting
editor.insertText(' Test!');
// TODO: Decreasing this below 100 causes the test to fail.
// See issue #11125.
await jest.advanceTimersByTimeAsync(450);
noteScreen.unmount();
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' });
}));
});
});
it('pressing "delete" should move the note to the trash', async () => {
@ -290,7 +286,7 @@ describe('screens/Note', () => {
await openNoteActionsMenu();
const deleteButton = await screen.findByText('Delete');
expect(deleteButton).toBeDisabled();
expect(deleteButton).toHaveProp('disabled', true);
act(() => cleanup());
});

View File

@ -148,7 +148,7 @@ interface State {
showSpeechToTextDialog: boolean;
}
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent {
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent<State> {
// This isn't in this.state because we don't want changing scroll to trigger
// a re-render.
private lastBodyScroll: number|undefined = undefined;
@ -725,9 +725,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
this.selection = { start: event.from, end: event.to };
};
public makeSaveAction() {
public makeSaveAction(state: State) {
return async () => {
return shared.saveNoteButton_press(this, null, null);
return shared.saveNoteButton_press(this, state, null, null);
};
}
@ -738,12 +738,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return this.saveActionQueues_[noteId];
}
public scheduleSave() {
this.saveActionQueue(this.state.note.id).push(this.makeSaveAction());
public scheduleSave(state: State) {
this.saveActionQueue(state.note.id).push(this.makeSaveAction(state));
}
private async saveNoteButton_press(folderId: string = null) {
await shared.saveNoteButton_press(this, folderId, null);
await shared.saveNoteButton_press(this, this.state, folderId, null);
Keyboard.dismiss();
}
@ -913,7 +913,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
void this.refreshResource(resource, newNote.body);
this.scheduleSave();
this.scheduleSave({ ...this.state, note: newNote });
return resource;
}
@ -1024,7 +1024,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
private toggleIsTodo_onPress() {
shared.toggleIsTodo_onPress(this);
this.scheduleSave();
this.scheduleSave(this.state);
}
private async share_onPress() {
@ -1473,7 +1473,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
const newNote: NoteEntity = { ...this.state.note };
newNote.body = `${newNote.body} ${text}`;
this.setState({ note: newNote });
this.scheduleSave();
this.scheduleSave(this.state);
} else {
if (this.useEditorBeta()) {
// We add a space so that if the feature is used twice in a row,

View File

@ -3,11 +3,10 @@ import { Store } from 'redux';
import { AppState } from '../../utils/types';
import TestProviderStack from '../testing/TestProviderStack';
import NoteRevisionViewer from './NoteRevisionViewer';
import { setupDatabaseAndSynchronizer, switchClient, revisionService, waitFor } from '@joplin/lib/testing/test-utils';
import { setupDatabaseAndSynchronizer, switchClient, revisionService } from '@joplin/lib/testing/test-utils';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import Note from '@joplin/lib/models/Note';
import { useMemo } from 'react';
import Revision from '@joplin/lib/models/Revision';
@ -86,20 +85,19 @@ describe('screens/NoteRevisionViewer', () => {
test('selecting a revision should render its content', async () => {
const note = await createNoteWithTestRevisions(3);
const { unmount } = render(<WrappedRevisionViewerScreen noteId={note.id}/>);
render(<WrappedRevisionViewerScreen noteId={note.id}/>);
const dropdown = screen.getByRole('button', { name: 'Select a revision...' });
fireEvent.press(dropdown);
// Select the second revision
await act(() => waitFor(async () => {
await waitFor(() => {
const firstRevision = screen.getAllByRole('menuitem')[1];
fireEvent.press(firstRevision);
}));
});
await act(() => waitFor(async () => {
await waitFor(async () => {
expect(await getRevisionViewerText()).toBe('Update 2');
}));
unmount();
});
});
});

View File

@ -6,7 +6,6 @@ import { Store } from 'redux';
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import { act, render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { AccessibilityActionInfo } from 'react-native';
import { setupDatabaseAndSynchronizer } from '@joplin/lib/testing/test-utils';
import Folder from '@joplin/lib/models/Folder';

View File

@ -2,8 +2,7 @@ import * as React from 'react';
import { ShareManagerComponent } from './index';
import Setting from '@joplin/lib/models/Setting';
import mockShareService from '@joplin/lib/testing/share/mockShareService';
import { fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { act, render, screen, userEvent, waitFor } from '@testing-library/react-native';
import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import ShareService from '@joplin/lib/services/share/ShareService';
@ -64,7 +63,9 @@ describe('ShareManager', () => {
// See https://github.com/callstack/react-native-testing-library/issues/809#issuecomment-984823700
const { refreshControl } = screen.getByTestId('refreshControl').props;
fireEvent(refreshControl, 'refresh');
await act(async () => {
await refreshControl.props.onRefresh();
});
// Should try to refresh shares
expect(getShareInvitationsMock).toHaveBeenCalled();

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { AppState } from '../../utils/types';
import { Store } from 'redux';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import '@testing-library/jest-native/extend-expect';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import TestProviderStack from '../testing/TestProviderStack';

View File

@ -8,7 +8,6 @@ import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import { getActiveMasterKeyId, setEncryptionEnabled, setMasterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { act, render, screen } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
interface WrapperProps { }

View File

@ -0,0 +1,97 @@
import Expo
import React
import ReactAppDependencyProvider
// Notes:
// - UNUserNotificationCenterDelegate is required by @react-native-community/push-notification-ios
// - This file is derived from the default React Native and Expo `AppDelegate.swift`.
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
// Define UNUserNotificationCenter -- required by @react-native-community/push-notification-ios
let center = UNUserNotificationCenter.current();
center.delegate = self;
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
// Quick actions
public override func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
RNQuickActionManager.onQuickActionPress(shortcutItem, completionHandler: completionHandler)
}
// Notifications with @react-native-community/push-notification-ios
// IOS 10+ Required for localNotification event
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
RNCPushNotificationIOS.didReceive(response);
completionHandler();
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}

View File

@ -1,5 +1,6 @@
#import <Expo/Expo.h>
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <RNCPushNotificationIOS.h>
#import "RNQuickActionManager.h"

View File

@ -7,11 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2F4891ED2DDDE04E0089027D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4891EC2DDDE04E0089027D /* AppDelegate.swift */; };
4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */; };
4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; };
57317DBFCCF429AEF0A019CB /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
AE152142260F770400217DCB /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE152141260F770400217DCB /* ShareViewController.m */; };
@ -51,14 +49,11 @@
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* JoplinTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JoplinTests.m; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* Joplin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Joplin.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Joplin/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.mm; path = Joplin/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Joplin/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Joplin/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Joplin/main.m; sourceTree = "<group>"; };
2F4891EC2DDDE04E0089027D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Joplin-Bridging-Header.h"; sourceTree = "<group>"; };
4D122472270878D700DE23E8 /* wtf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wtf.swift; sourceTree = "<group>"; };
78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.debug.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.debug.xcconfig"; sourceTree = "<group>"; };
800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Joplin/LaunchScreen.storyboard; sourceTree = "<group>"; };
@ -118,15 +113,12 @@
13B07FAE1A68108700A75B9A /* Joplin */ = {
isa = PBXGroup;
children = (
2F4891EC2DDDE04E0089027D /* AppDelegate.swift */,
AE7945E6259C9AEE00051BE2 /* Joplin.entitlements */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB71A68108700A75B9A /* main.m */,
4D122472270878D700DE23E8 /* wtf.swift */,
4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */,
);
name = Joplin;
@ -499,9 +491,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
4D122473270878D700DE23E8 /* wtf.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
2F4891ED2DDDE04E0089027D /* AppDelegate.swift in Sources */,
4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -654,7 +644,9 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD = "";
LDPLUSPLUS = "";
@ -678,10 +670,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@ -732,7 +721,9 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD = "";
LDPLUSPLUS = "";
@ -755,10 +746,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@ -1,8 +0,0 @@
#import <RCTAppDelegate.h>
#import <Expo/Expo.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UNUserNotificationCenter.h>
@interface AppDelegate : EXAppDelegateWrapper
@end

View File

@ -1,97 +0,0 @@
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
#import <RNCPushNotificationIOS.h>
#import "RNQuickActionManager.h"
@implementation AppDelegate
// ===================================================
// BEGIN Linking support
// ===================================================
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
// ===================================================
// END Linking support
// ===================================================
// ===================================================
// BEGIN react-native-quick-actions
// ===================================================
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded)) completionHandler {
[RNQuickActionManager onQuickActionPress:shortcutItem completionHandler:completionHandler];
}
// ===================================================
// END react-native-quick-actions
// ===================================================
// ===================================================
// BEGIN react-native-push-notification-ios
// ===================================================
// IOS 10+ Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
[RNCPushNotificationIOS didReceiveNotificationResponse:response];
completionHandler();
}
// IOS 4-10 Required for the localNotification event.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
[RNCPushNotificationIOS didReceiveLocalNotification:notification];
}
// ===================================================
// END react-native-push-notification-ios
// ===================================================
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"main";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
// BEGIN react-native-push-notification-ios
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
// END react-native-push-notification-ios
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@end

View File

@ -1,10 +0,0 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@ -1,43 +1,40 @@
# Use the Legacy Architecture:
# See https://blog.logrocket.com/react-native-new-architecture-sync-async-rendering
# https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
"react-native/scripts/react_native_pods.rb",
{paths: [process.argv[1]]},
)', __dir__]).strip
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
# From Expo
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
:deterministic_uuids => false
# Note: it was 13.4 to get @react-native-community/datetimepicker to work but
# it's probably not necessary actually. Just needed to upgrade XCode.
#
# 2021-11-04: Set to 13.0 because it crashes with 12.x
# https://github.com/laurent22/joplin/issues/5671
#
# 2021-12-17: Changed back to 11.0 because after the fix it works with at least
# 12.x, and probably 11.0 too, which is the version supported by React Native.
platform :ios, min_ios_version_supported
prepare_react_native_project!
linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
target 'Joplin' do
use_expo_modules!
post_integrate do |installer|
begin
expo_patch_react_imports!(installer)
rescue => e
Pod::UI.warn e
end
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
{
"newArchEnabled": "false"
}

View File

@ -1,15 +0,0 @@
//
// wtf.swift
// Joplin
//
// Created by Laurent on 02/10/2021.
// Copyright © 2021 joplinapp.org. All rights reserved.
//
// For whatever reason, this empty file is needed to fix this bug:
// https://github.com/facebook/react-native/issues/32242
//
// Solution:
// https://github.com/facebook/react-native/issues/32242#issuecomment-924770488
import Foundation

View File

@ -29,7 +29,7 @@
"@joplin/renderer": "~3.4",
"@joplin/utils": "~3.4",
"@react-native-clipboard/clipboard": "1.14.3",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/datetimepicker": "8.3.0",
"@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
@ -41,16 +41,16 @@
"crypto-browserify": "3.12.1",
"deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0",
"expo": "52.0.46",
"expo-av": "15.0.2",
"expo-camera": "16.0.18",
"expo": "53.0.9",
"expo-av": "15.1.4",
"expo-camera": "16.1.6",
"lodash": "4.17.21",
"md5": "2.3.0",
"path-browserify": "1.0.1",
"prop-types": "15.8.1",
"punycode": "2.3.1",
"react": "18.3.1",
"react-native": "0.77.2",
"react": "19.0.0",
"react-native": "0.79.2",
"react-native-device-info": "10.14.0",
"react-native-dropdownalert": "5.1.0",
"react-native-exit-app": "2.0.0",
@ -97,16 +97,15 @@
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.77.2",
"@react-native/metro-config": "0.77.2",
"@react-native/typescript-config": "0.77.2",
"@react-native/babel-preset": "0.79.2",
"@react-native/metro-config": "0.79.2",
"@react-native/typescript-config": "0.79.2",
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
"@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.3.3",
"@testing-library/react-native": "13.2.0",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/node": "18.19.67",
"@types/react": "18.3.18",
"@types/react": "19.0.10",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.123",
"@types/tar-stream": "3.1.3",
@ -124,10 +123,10 @@
"jsdom": "24.1.3",
"nodemon": "3.1.7",
"punycode": "2.3.1",
"react-dom": "18.3.1",
"react-native-web": "0.19.13",
"react-dom": "19.0.0",
"react-native-web": "0.20.0",
"react-refresh": "0.16.0",
"react-test-renderer": "18.3.1",
"react-test-renderer": "19.0.0",
"sharp": "0.33.5",
"sqlite3": "5.1.6",
"timers-browserify": "2.0.12",

View File

@ -31,14 +31,31 @@ export interface Props {
sharedData: SharedData|undefined;
}
export interface BaseNoteScreenComponent {
props: Props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
state: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
setState: (newState: any)=> void;
export interface BaseState {
note: NoteEntity;
lastSavedNote: NoteEntity;
newAndNoTitleChangeNoteId: boolean;
mode: string;
folder: FolderEntity;
isLoading: boolean;
fromShare: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
noteResources: any;
readOnly: boolean;
noteLastLoadTime: number;
}
scheduleSave(): void;
export interface BaseNoteScreenComponent<State extends BaseState = BaseState> {
props: Props;
state: State;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
setState(state: any): void;
// To prevent race conditions, scheduleSave takes a snapshot of the
// current state. Previously, the delay between calling setState(state) and
// this.state getting the new state value could cause the wrong state
// to be saved.
scheduleSave(currentState: State): void;
scheduleFocusUpdate(): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
attachFile(asset: any, fileType: any): void;
@ -49,7 +66,7 @@ interface Shared {
noteExists?: (noteId: string)=> Promise<boolean>;
handleNoteDeletedWhileEditing_?: (note: NoteEntity)=> Promise<NoteEntity>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
saveNoteButton_press?: (comp: BaseNoteScreenComponent, folderId: string, options: any)=> Promise<void>;
saveNoteButton_press?: (comp: BaseNoteScreenComponent, state: BaseState, folderId: string, options: any)=> Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
saveOneProperty?: (comp: BaseNoteScreenComponent, name: string, value: any)=> void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -97,12 +114,13 @@ shared.handleNoteDeletedWhileEditing_ = async (note: NoteEntity) => {
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, folderId: string = null, options: any = null) {
shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, state: BaseState, folderId: string = null, options: any = null) {
options = { autoTitle: true, ...options };
state = { ...comp.state, ...state };
const releaseMutex = await saveNoteMutex_.acquire();
let note = { ...comp.state.note };
let note = { ...state.note };
const recreatedNote = await shared.handleNoteDeletedWhileEditing_(note);
if (recreatedNote) note = recreatedNote;
@ -121,11 +139,11 @@ shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, fold
const saveOptions = {
userSideValidation: true,
fields: BaseModel.diffObjectsFields(comp.state.lastSavedNote, note),
fields: BaseModel.diffObjectsFields(state.lastSavedNote, note),
dispatchOptions: { preserveSelection: true },
};
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isProvisionalNote && !note.title);
const hasAutoTitle = state.newAndNoTitleChangeNoteId || (isProvisionalNote && !note.title);
if (hasAutoTitle && options.autoTitle) {
note.title = Note.defaultTitle(note.body);
if (saveOptions.fields && saveOptions.fields.indexOf('title') < 0) saveOptions.fields.push('title');
@ -133,7 +151,7 @@ shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, fold
const savedNote = 'fields' in saveOptions && !saveOptions.fields.length ? { ...note } : await Note.save(note, saveOptions);
const stateNote = comp.state.note;
const stateNote = state.note;
// Note was reloaded while being saved.
if (!recreatedNote && (!stateNote || stateNote.id !== savedNote.id)) return releaseMutex();
@ -169,7 +187,7 @@ shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, fold
const updateGeoloc = async () => {
const geoNote: NoteEntity = await Note.updateGeolocation(note.id);
const stateNote = comp.state.note;
const stateNote = state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
@ -183,7 +201,7 @@ shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, fold
};
const modNote = { ...stateNote, ...geoInfo };
const modLastSavedNote = { ...comp.state.lastSavedNote, ...geoInfo };
const modLastSavedNote = { ...state.lastSavedNote, ...geoInfo };
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
};
@ -206,7 +224,7 @@ shared.saveOneProperty = async function(comp: BaseNoteScreenComponent, name: str
let toSave: any = { id: note.id };
toSave[name] = value;
toSave = await Note.save(toSave);
note[name] = toSave[name];
(note as Record<string, unknown>)[name] = toSave[name];
comp.setState({
lastSavedNote: { ...note },
@ -220,11 +238,11 @@ shared.noteComponent_change = function(comp: BaseNoteScreenComponent, propName:
const newState: any = {};
const note = { ...comp.state.note };
note[propName] = propValue;
(note as Record<string, unknown>)[propName] = propValue;
newState.note = note;
comp.setState(newState);
comp.scheduleSave();
comp.scheduleSave(newState);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

1742
yarn.lock

File diff suppressed because it is too large Load Diff