fix(ios): stabilize keyboard toolbar, image picker, and scrolling (#15182)

## Summary
- keep the editor active when iOS keyboard toolbar interactions move
focus into range-sync excluded widgets
- use the native iOS image picker/source sheet and sync native
presentation with the app theme
- pin `ListViewKit` to `1.1.6` so the iOS workspace resolves with Xcode
16.3
- restore vertical scrolling in the iOS `WKWebView` by removing the
global `contentOffset` reset while preserving zoom prevention

## Test plan
- [x] `yarn vitest --run --config \"vitest.config.ts\"
--browser.enabled=false \"src/__tests__/inline/active.unit.spec.ts\"`
- [x] `LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild
-resolvePackageDependencies -workspace
\"packages/frontend/apps/ios/App/App.xcworkspace\" -scheme \"App\"`
- [x] `xcodebuild -workspace \"App.xcworkspace\" -scheme \"App\"
-destination \"generic/platform=iOS Simulator\" build
CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES ARCHS=arm64`
- [x] Xcode build validation for the updated PR branch
This commit is contained in:
keepClamDown
2026-07-09 11:11:55 +08:00
committed by GitHub
parent 9f8e5c0eb3
commit a868f54eeb
19 changed files with 1095 additions and 28 deletions
+52
View File
@@ -54,9 +54,11 @@ import {
MarkdownAdapter,
titleMiddleware,
} from '@blocksuite/affine/shared/adapters';
import { registerNativeImageFilesPicker } from '@blocksuite/affine/shared/utils';
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
import { App as CapacitorApp } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
import { Capacitor } from '@capacitor/core';
import { Haptics } from '@capacitor/haptics';
import { Keyboard, KeyboardStyle } from '@capacitor/keyboard';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
@@ -69,8 +71,10 @@ import { RouterProvider } from 'react-router-dom';
import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
import { ModalConfigProvider } from './modal-config';
import { AffineTheme } from './plugins/affine-theme';
import { Auth } from './plugins/auth';
import { Hashcash } from './plugins/hashcash';
import { ImagePicker } from './plugins/image-picker';
import { NbStoreNativeDBApis } from './plugins/nbstore';
import { PayWall } from './plugins/paywall';
import { Preview } from './plugins/preview';
@@ -237,6 +241,39 @@ registerNativePreviewHandlers({
renderMermaidSvg: request => Preview.renderMermaidSvg(request),
renderTypstSvg: request => Preview.renderTypstSvg(request),
});
registerNativeImageFilesPicker(async () => {
const result = await ImagePicker.pickImages({ multiple: true });
if (result.canceled || result.files.length === 0) {
return [];
}
const settled = await Promise.allSettled(
result.files.map(async file => {
const filePath = file.path.startsWith('file://')
? file.path
: `file://${file.path}`;
const response = await fetch(Capacitor.convertFileSrc(filePath));
if (!response.ok) {
throw new Error(
`Failed to read image picker file: ${file.name} (status ${response.status})`
);
}
const blob = await response.blob();
return new File([blob], file.name, {
type: file.mimeType || blob.type || 'image/*',
lastModified: file.lastModified,
});
})
);
return settled
.filter(
(settledResult): settledResult is PromiseFulfilledResult<File> =>
settledResult.status === 'fulfilled'
)
.map(settledResult => settledResult.value);
});
// ------ some apis for native ------
(window as any).getCurrentServerBaseUrl = () => {
@@ -252,6 +289,9 @@ registerNativePreviewHandlers({
(window as any).getCurrentI18nLocale = () => {
return I18n.language;
};
(window as any).getCurrentThemeMode = () => {
return 'system';
};
(window as any).getAiButtonFeatureFlag = () => {
const featureFlagService = frameworkProvider.get(FeatureFlagService);
return featureFlagService.flags.enable_mobile_ai_button.value;
@@ -522,6 +562,18 @@ const KeyboardThemeProvider = () => {
});
}, [resolvedTheme]);
useEffect(() => {
const themeMode = resolvedTheme === 'dark' ? 'dark' : 'light';
(window as any).getCurrentThemeMode = () => {
return themeMode;
};
AffineTheme.onThemeChanged({
themeMode,
}).catch(e => {
console.error(`Failed to sync app theme: ${e}`);
});
}, [resolvedTheme]);
return null;
};
@@ -0,0 +1,5 @@
import type { NativeThemeMode } from './theme-mode';
export interface AffineThemePlugin {
onThemeChanged(options: { themeMode: NativeThemeMode }): Promise<void>;
}
@@ -0,0 +1,9 @@
import { registerPlugin } from '@capacitor/core';
import type { AffineThemePlugin } from './definitions';
const AffineTheme = registerPlugin<AffineThemePlugin>('AffineTheme');
export * from './definitions';
export * from './theme-mode';
export { AffineTheme };
@@ -0,0 +1 @@
export type NativeThemeMode = 'dark' | 'light' | 'system';
@@ -0,0 +1,13 @@
export interface PickedImageFile {
path: string;
name: string;
mimeType: string;
lastModified: number;
}
export interface ImagePickerPlugin {
pickImages(options?: { multiple?: boolean }): Promise<{
files: PickedImageFile[];
canceled?: boolean;
}>;
}
@@ -0,0 +1,8 @@
import { registerPlugin } from '@capacitor/core';
import type { ImagePickerPlugin } from './definitions';
const ImagePicker = registerPlugin<ImagePickerPlugin>('ImagePicker');
export * from './definitions';
export { ImagePicker };