聊天(conversation/[id]) - 根布局挂载 KeyboardProvider,会话页使用 react-native-keyboard-controller 的 KeyboardAvoidingView(padding + 仅文字模式 enabled),替代手写 keyboardLift 与 RN KeyboardAvoidingView 分端逻辑,改善 Android 键盘遮挡与布局一致性。 - 键盘:keyboardDidShow 后 scrollToEnd;iOS 用 keyboardWillShow 提前更新键盘可见状态; 收起使用 WillHide/DidHide;监听在 effect 中统一移除。 - 输入框高度:ChatInputBar 通过 onInputDisplayHeightChange 在 inputDisplayHeight 变化时 触发滚到底;保留底部容器 onLayout 以覆盖连接提示与整块高度变化。 配置与构建 - app.config:移除 web 块与 expo-sqlite Web 所需的 COEP/COOP headers;expo-router 插件 改为无参;Android 显式 softwareKeyboardLayoutMode: resize。 - metro.config:移除 wasm 资源与 COOP/COOP dev server OC
213 lines
6.4 KiB
TypeScript
213 lines
6.4 KiB
TypeScript
import type { ConfigContext, ExpoConfig } from 'expo/config';
|
||
|
||
import zh from './locales/zh.json';
|
||
import en from './locales/en.json';
|
||
import permissionsZh from './locales/permissions/zh.json';
|
||
import permissionsEn from './locales/permissions/en.json';
|
||
|
||
type PermissionKey =
|
||
| 'microphone'
|
||
| 'photos'
|
||
| 'camera'
|
||
| 'microphoneForVideo'
|
||
| 'faceId';
|
||
|
||
type LocaleMessages = {
|
||
permissions?: Partial<Record<PermissionKey, string>>;
|
||
};
|
||
|
||
type PrivacyAccessedAPIType = {
|
||
NSPrivacyAccessedAPIType: string;
|
||
NSPrivacyAccessedAPITypeReasons: string[];
|
||
};
|
||
|
||
const LOCALES: Record<string, LocaleMessages> = {
|
||
zh: { ...zh, permissions: permissionsZh },
|
||
en: { ...en, permissions: permissionsEn },
|
||
};
|
||
|
||
const SUPPORTED_LOCALES = ['zh', 'en'] as const;
|
||
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
|
||
|
||
const PERMISSION_FALLBACKS: Record<PermissionKey, string> = {
|
||
microphone: 'Allow $(PRODUCT_NAME) to access your microphone.',
|
||
photos: 'Allow $(PRODUCT_NAME) to access your photos.',
|
||
camera: 'Allow $(PRODUCT_NAME) to access your camera.',
|
||
microphoneForVideo: 'Allow $(PRODUCT_NAME) to access your microphone.',
|
||
faceId: 'Allow $(PRODUCT_NAME) to access Face ID.',
|
||
};
|
||
|
||
/**
|
||
* iOS privacy manifest required-reason APIs.
|
||
*
|
||
* These values are aggregated from the PrivacyInfo.xcprivacy files shipped by the
|
||
* Expo native modules currently used in this app, because Apple may not always
|
||
* correctly parse all static CocoaPods dependency manifests during submission.
|
||
*
|
||
* Sources checked in node_modules:
|
||
* - expo-task-manager
|
||
* - expo-media-library
|
||
* - expo-localization
|
||
* - expo-file-system
|
||
* - expo-system-ui
|
||
* - expo-device
|
||
* - expo-constants
|
||
*/
|
||
const REQUIRED_PRIVACY_ACCESSED_API_TYPES: PrivacyAccessedAPIType[] = [
|
||
{
|
||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryUserDefaults',
|
||
NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
|
||
},
|
||
{
|
||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryFileTimestamp',
|
||
NSPrivacyAccessedAPITypeReasons: ['0A2A.1', '3B52.1'],
|
||
},
|
||
{
|
||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
|
||
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
|
||
},
|
||
{
|
||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategorySystemBootTime',
|
||
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
|
||
},
|
||
];
|
||
|
||
const localePermissions = LOCALES[PRIMARY_LOCALE]?.permissions ?? {};
|
||
|
||
function getPermissionMessage(key: PermissionKey): string {
|
||
return localePermissions[key] ?? PERMISSION_FALLBACKS[key];
|
||
}
|
||
|
||
function mergePrivacyAccessedApiTypes(
|
||
existing: PrivacyAccessedAPIType[] = [],
|
||
required: PrivacyAccessedAPIType[] = [],
|
||
): PrivacyAccessedAPIType[] {
|
||
const merged = new Map<string, Set<string>>();
|
||
|
||
for (const item of [...existing, ...required]) {
|
||
const reasons =
|
||
merged.get(item.NSPrivacyAccessedAPIType) ?? new Set<string>();
|
||
for (const reason of item.NSPrivacyAccessedAPITypeReasons) {
|
||
reasons.add(reason);
|
||
}
|
||
merged.set(item.NSPrivacyAccessedAPIType, reasons);
|
||
}
|
||
|
||
return Array.from(merged.entries()).map(([apiType, reasons]) => ({
|
||
NSPrivacyAccessedAPIType: apiType,
|
||
NSPrivacyAccessedAPITypeReasons: Array.from(reasons),
|
||
}));
|
||
}
|
||
export default ({ config }: ConfigContext): ExpoConfig => {
|
||
const existingPrivacyAccessedApiTypes = (config?.ios?.privacyManifests
|
||
?.NSPrivacyAccessedAPITypes ?? []) as PrivacyAccessedAPIType[];
|
||
|
||
return {
|
||
...config,
|
||
name: 'Life Echo',
|
||
slug: 'life-echo',
|
||
version: '1.0.0',
|
||
orientation: 'portrait',
|
||
icon: './assets/images/icon.png',
|
||
scheme: 'lifeecho',
|
||
userInterfaceStyle: 'automatic',
|
||
ios: {
|
||
...config?.ios,
|
||
icon: './assets/images/icon.png',
|
||
bundleIdentifier: 'com.anonymous.app-expo',
|
||
config: {
|
||
usesNonExemptEncryption: false,
|
||
},
|
||
infoPlist: {
|
||
...config?.ios?.infoPlist,
|
||
CFBundleAllowMixedLocalizations: true,
|
||
},
|
||
privacyManifests: {
|
||
...config?.ios?.privacyManifests,
|
||
NSPrivacyAccessedAPITypes: mergePrivacyAccessedApiTypes(
|
||
existingPrivacyAccessedApiTypes,
|
||
REQUIRED_PRIVACY_ACCESSED_API_TYPES,
|
||
),
|
||
},
|
||
},
|
||
android: {
|
||
...config?.android,
|
||
/**
|
||
* `resize` → `adjustResize`:键盘顶起时缩小窗口,聊天输入框随内容上移。
|
||
* 与 `KeyboardAvoidingView` + `behavior="height"` 叠用会重复处理,易错位(见 Expo Keyboard 指南)。
|
||
*/
|
||
softwareKeyboardLayoutMode: 'resize',
|
||
// Reverse-DNS; no hyphens (Android package name rules). Matches iOS bundle id intent.
|
||
package: 'com.anonymous.appexpo',
|
||
adaptiveIcon: {
|
||
backgroundColor: '#E6F4FE',
|
||
foregroundImage: './assets/images/android-icon-foreground.png',
|
||
monochromeImage: './assets/images/android-icon-monochrome.png',
|
||
},
|
||
predictiveBackGestureEnabled: false,
|
||
},
|
||
plugins: [
|
||
// CI/local release: android/app/keystore.properties + store file → release signing; -PversionName/-PversionCode
|
||
'./plugins/withAndroidReleaseSigning',
|
||
'expo-router',
|
||
[
|
||
'expo-splash-screen',
|
||
{
|
||
// 与 android.adaptiveIcon.backgroundColor、品牌浅紫一致(见 scripts/generate-app-icon.sh)
|
||
backgroundColor: '#E6F4FE',
|
||
image: './assets/images/splash-icon.png',
|
||
resizeMode: 'contain',
|
||
imageWidth: 200,
|
||
},
|
||
],
|
||
[
|
||
'expo-localization',
|
||
{
|
||
supportedLocales: {
|
||
ios: SUPPORTED_LOCALES,
|
||
android: SUPPORTED_LOCALES,
|
||
},
|
||
},
|
||
],
|
||
'expo-asset',
|
||
[
|
||
'expo-audio',
|
||
{
|
||
microphonePermission: getPermissionMessage('microphone'),
|
||
},
|
||
],
|
||
[
|
||
'expo-image-picker',
|
||
{
|
||
photosPermission: getPermissionMessage('photos'),
|
||
cameraPermission: getPermissionMessage('camera'),
|
||
microphonePermission: getPermissionMessage('microphoneForVideo'),
|
||
},
|
||
],
|
||
'expo-image',
|
||
'expo-sqlite',
|
||
[
|
||
'expo-secure-store',
|
||
{
|
||
configureAndroidBackup: true,
|
||
faceIDPermission: getPermissionMessage('faceId'),
|
||
},
|
||
],
|
||
'expo-font',
|
||
],
|
||
locales: {
|
||
zh: './locales/zh.json',
|
||
en: './locales/en.json',
|
||
},
|
||
extra: {
|
||
...config?.extra,
|
||
supportsRTL: true,
|
||
},
|
||
experiments: {
|
||
...config?.experiments,
|
||
typedRoutes: true,
|
||
reactCompiler: true,
|
||
},
|
||
};
|
||
};
|