chore: remove IDE config files and improve web platform compatibility
- Remove `.idea/` IntelliJ configuration files from version control - Add web-specific touch handling for swipeable message bubbles and schedule screen - Fix CSS touch-action rules for better web scrolling behavior - Add nestedScrollEnabled to ScrollViews for proper gesture handling - Improve null safety checks in profile screens - Add horizontal ScrollView wrapper for notification filter tags - Add hasHeader prop support for embedded profile screens
This commit is contained in:
10
.idea/.gitignore
generated
vendored
10
.idea/.gitignore
generated
vendored
@@ -1,10 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Screenshots
|
|
||||||
screenshots/
|
|
||||||
9
.idea/frontend.iml
generated
9
.idea/frontend.iml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
448
.idea/gradle.xml
generated
448
.idea/gradle.xml
generated
@@ -1,448 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-application/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-application/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-constants/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-font/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-font/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-image/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linking/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-linking/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-router/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-router/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-updates/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-video/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo-video/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
|
|
||||||
<option name="gradleJvm" value="ms-21" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="ProjectRootManager">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -53,24 +53,33 @@ if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|||||||
}
|
}
|
||||||
*:focus { outline: none !important; }
|
*:focus { outline: none !important; }
|
||||||
*:focus-visible { outline: none !important; }
|
*:focus-visible { outline: none !important; }
|
||||||
/* 修复移动端 FlatList/ScrollView 滑动问题 */
|
/* 修复移动端滑动问题 - 仅对根容器限制 */
|
||||||
html, body {
|
html, body {
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
touch-action: pan-y;
|
touch-action: manipulation;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
/* React Native Web 生成的滚动容器 */
|
/* React Native Web 生成的滚动容器 - 允许双向滑动 */
|
||||||
[class*="css-"] {
|
[class*="css-view"] {
|
||||||
touch-action: pan-y !important;
|
touch-action: auto;
|
||||||
-webkit-overflow-scrolling: touch !important;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
/* 确保所有可滚动元素都可以垂直滑动 */
|
/* 水平滚动容器 */
|
||||||
div[style*="overflow"] {
|
[data-horizontal-scroll="true"],
|
||||||
touch-action: pan-y !important;
|
div[style*="overflow-x"],
|
||||||
-webkit-overflow-scrolling: touch !important;
|
div[style*="overflow: scroll"],
|
||||||
|
div[style*="overflow: auto"] {
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
/* 禁用水平滑动,只允许垂直滑动 */
|
/* 手势区域 - 允许滑动手势 */
|
||||||
* {
|
[data-gesture-area="true"] {
|
||||||
touch-action: pan-y pinch-zoom;
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
/* 图片查看器手势区域 */
|
||||||
|
.react-native-image-viewer,
|
||||||
|
[data-image-viewer="true"] {
|
||||||
|
touch-action: pinch-zoom pan-x pan-y;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
statusBarTranslucent
|
statusBarTranslucent
|
||||||
>
|
>
|
||||||
<GestureHandlerRootView style={styles.root}>
|
<GestureHandlerRootView style={styles.root} data-image-viewer="true">
|
||||||
<View style={[styles.container, { backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity})` }]}>
|
<View style={[styles.container, { backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity})` }]}>
|
||||||
{/* 顶部控制栏 */}
|
{/* 顶部控制栏 */}
|
||||||
{showControls && (
|
{showControls && (
|
||||||
|
|||||||
@@ -676,6 +676,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
ref={capsuleHScrollRef}
|
ref={capsuleHScrollRef}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
nestedScrollEnabled
|
||||||
contentContainerStyle={styles.capsuleList}
|
contentContainerStyle={styles.capsuleList}
|
||||||
onScroll={onCapsuleHorizontalScroll}
|
onScroll={onCapsuleHorizontalScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
|
|||||||
@@ -1182,7 +1182,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{currentUser?.id === post.author?.id && (
|
{!!currentUser && currentUser.id === post.author?.id && (
|
||||||
<View style={styles.metaActions}>
|
<View style={styles.metaActions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.editButtonInline}
|
style={styles.editButtonInline}
|
||||||
@@ -1214,8 +1214,8 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{/* 举报按钮 - 只对非帖子作者显示 */}
|
{/* 举报按钮 - 只对已登录且非帖子作者显示 */}
|
||||||
{currentUser?.id !== post.author?.id && (
|
{!!currentUser && currentUser.id !== post.author?.id && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.reportButtonInline}
|
style={styles.reportButtonInline}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
|
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={[styles.container, { overflow: 'hidden' }]}
|
style={[styles.container, Platform.OS !== 'web' && { overflow: 'hidden' }]}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
BackHandler,
|
BackHandler,
|
||||||
Animated,
|
Animated,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useIsFocused } from '@react-navigation/native';
|
import { useIsFocused } from '@react-navigation/native';
|
||||||
@@ -444,6 +445,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
{/* 分类筛选 */}
|
{/* 分类筛选 */}
|
||||||
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
|
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.filterList}
|
||||||
|
>
|
||||||
{MESSAGE_TYPES.map(type => {
|
{MESSAGE_TYPES.map(type => {
|
||||||
const count = type.key === 'all'
|
const count = type.key === 'all'
|
||||||
? displayMessages.length
|
? displayMessages.length
|
||||||
@@ -469,9 +475,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
variant="body"
|
style={isActive ? { ...styles.filterTagText, ...styles.filterTagTextActive } : styles.filterTagText}
|
||||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
|
||||||
style={isActive ? styles.filterTagTextActive : undefined}
|
|
||||||
>
|
>
|
||||||
{type.title}
|
{type.title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -479,7 +483,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||||
<Text
|
<Text
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
color={isActive ? colors.primary.main : colors.text.hint}
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -488,6 +492,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
@@ -526,6 +531,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
{/* 分类筛选 */}
|
{/* 分类筛选 */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.filterList}
|
||||||
|
>
|
||||||
{MESSAGE_TYPES.map(type => {
|
{MESSAGE_TYPES.map(type => {
|
||||||
const count = type.key === 'all'
|
const count = type.key === 'all'
|
||||||
? displayMessages.length
|
? displayMessages.length
|
||||||
@@ -550,9 +560,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
variant="body"
|
style={isActive ? { ...styles.filterTagText, ...styles.filterTagTextActive } : styles.filterTagText}
|
||||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
|
||||||
style={isActive ? styles.filterTagTextActive : undefined}
|
|
||||||
>
|
>
|
||||||
{type.title}
|
{type.title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -560,7 +568,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||||
<Text
|
<Text
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
color={isActive ? colors.primary.main : colors.text.hint}
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -569,6 +577,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
@@ -677,52 +686,51 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
// 分类筛选 - 扁平化风格(分段式设计)
|
// 分类筛选 - 扁平化风格(分段式设计)
|
||||||
filterContainer: {
|
filterContainer: {
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.divider || '#E5E5EA',
|
borderBottomColor: colors.divider || '#E5E5EA',
|
||||||
},
|
},
|
||||||
filterContainerWideWeb: {
|
filterContainerWideWeb: {
|
||||||
paddingHorizontal: 32,
|
alignItems: 'center',
|
||||||
paddingVertical: 16,
|
},
|
||||||
justifyContent: 'center',
|
filterList: {
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
gap: spacing.xs,
|
||||||
|
paddingLeft: spacing.lg,
|
||||||
|
paddingRight: spacing.lg,
|
||||||
},
|
},
|
||||||
filterTag: {
|
filterTag: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: 8,
|
paddingVertical: spacing.xs,
|
||||||
marginRight: 8,
|
borderRadius: borderRadius.full,
|
||||||
borderRadius: 14,
|
|
||||||
backgroundColor: colors.background.default,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'transparent',
|
|
||||||
},
|
},
|
||||||
filterTagWide: {
|
filterTagWide: {
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingVertical: 10,
|
paddingVertical: spacing.sm,
|
||||||
marginRight: 10,
|
|
||||||
},
|
},
|
||||||
filterTagActive: {
|
filterTagActive: {
|
||||||
backgroundColor: colors.primary.main,
|
},
|
||||||
borderColor: colors.primary.main,
|
filterTagText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text.secondary,
|
||||||
},
|
},
|
||||||
filterTagTextActive: {
|
filterTagTextActive: {
|
||||||
fontWeight: '600',
|
color: colors.primary.main,
|
||||||
},
|
},
|
||||||
filterCount: {
|
filterCount: {
|
||||||
marginLeft: 6,
|
marginLeft: spacing.xs,
|
||||||
backgroundColor: colors.primary.light + '40',
|
backgroundColor: colors.background.default,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: spacing.xs,
|
||||||
paddingVertical: 1,
|
paddingVertical: 1,
|
||||||
borderRadius: 8,
|
borderRadius: borderRadius.full,
|
||||||
minWidth: 18,
|
minWidth: 18,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
filterCountActive: {
|
filterCountActive: {
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
backgroundColor: colors.primary.light + '30',
|
||||||
},
|
},
|
||||||
listContent: {
|
listContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useCallback } from 'react';
|
import React, { useRef, useCallback } from 'react';
|
||||||
import { View, StyleSheet, Animated } from 'react-native';
|
import { View, StyleSheet, Animated, Platform } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { useAppColors } from '../../../../theme';
|
import { useAppColors } from '../../../../theme';
|
||||||
|
|
||||||
// 滑动阈值
|
|
||||||
const SWIPE_THRESHOLD = 30;
|
const SWIPE_THRESHOLD = 30;
|
||||||
const MAX_SWIPE_DISTANCE = 50;
|
const MAX_SWIPE_DISTANCE = 50;
|
||||||
|
|
||||||
@@ -31,45 +30,39 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
|||||||
const translateX = useRef(new Animated.Value(0)).current;
|
const translateX = useRef(new Animated.Value(0)).current;
|
||||||
const hasTriggeredRef = useRef(false);
|
const hasTriggeredRef = useRef(false);
|
||||||
|
|
||||||
// 处理手势事件 - 使用原生驱动,不干预值
|
|
||||||
const onGestureEvent = Animated.event(
|
const onGestureEvent = Animated.event(
|
||||||
[{ nativeEvent: { translationX: translateX } }],
|
[{ nativeEvent: { translationX: translateX } }],
|
||||||
{ useNativeDriver: true }
|
{ useNativeDriver: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理手势状态变化
|
|
||||||
const onHandlerStateChange = useCallback((event: any) => {
|
const onHandlerStateChange = useCallback((event: any) => {
|
||||||
const { nativeEvent } = event;
|
const { nativeEvent } = event;
|
||||||
|
|
||||||
if (nativeEvent.state === State.END) {
|
if (nativeEvent.state === State.END) {
|
||||||
const translationX = nativeEvent.translationX;
|
const translationX = nativeEvent.translationX;
|
||||||
|
|
||||||
// 只允许特定方向的滑动
|
|
||||||
const shouldTrigger = isMe
|
const shouldTrigger = isMe
|
||||||
? translationX < -SWIPE_THRESHOLD // 自己消息只能向左滑
|
? translationX < -SWIPE_THRESHOLD
|
||||||
: translationX > SWIPE_THRESHOLD; // 对方消息只能向右滑
|
: translationX > SWIPE_THRESHOLD;
|
||||||
|
|
||||||
if (shouldTrigger && !hasTriggeredRef.current) {
|
if (shouldTrigger && !hasTriggeredRef.current) {
|
||||||
hasTriggeredRef.current = true;
|
hasTriggeredRef.current = true;
|
||||||
|
|
||||||
// 触觉反馈
|
if (Platform.OS !== 'web') {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
}
|
||||||
|
|
||||||
// 立即触发回复
|
|
||||||
onReply();
|
onReply();
|
||||||
|
|
||||||
// 重置标记
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hasTriggeredRef.current = false;
|
hasTriggeredRef.current = false;
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即回到原位
|
|
||||||
translateX.setValue(0);
|
translateX.setValue(0);
|
||||||
}
|
}
|
||||||
}, [isMe, onReply]);
|
}, [isMe, onReply]);
|
||||||
|
|
||||||
// 计算回复图标的透明度 - 只在允许的方向显示
|
|
||||||
const iconOpacity = translateX.interpolate({
|
const iconOpacity = translateX.interpolate({
|
||||||
inputRange: isMe
|
inputRange: isMe
|
||||||
? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10]
|
? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10]
|
||||||
@@ -78,7 +71,6 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
|||||||
extrapolate: 'clamp',
|
extrapolate: 'clamp',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算消息的位移 - 限制在允许的方向
|
|
||||||
const clampedTranslateX = translateX.interpolate({
|
const clampedTranslateX = translateX.interpolate({
|
||||||
inputRange: isMe
|
inputRange: isMe
|
||||||
? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10]
|
? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10]
|
||||||
@@ -89,21 +81,117 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
|||||||
extrapolate: 'clamp',
|
extrapolate: 'clamp',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果禁用手势,直接返回子元素
|
const webTouchStartRef = useRef<{ x: number; time: number } | null>(null);
|
||||||
|
const webTranslateXRef = useRef(new Animated.Value(0)).current;
|
||||||
|
const webIconOpacity = webTranslateXRef.interpolate({
|
||||||
|
inputRange: isMe
|
||||||
|
? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10]
|
||||||
|
: [-10, 0, SWIPE_THRESHOLD, MAX_SWIPE_DISTANCE],
|
||||||
|
outputRange: isMe ? [1, 0.8, 0, 0] : [0, 0, 0.8, 1],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
const webClampedTranslateX = webTranslateXRef.interpolate({
|
||||||
|
inputRange: isMe
|
||||||
|
? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10]
|
||||||
|
: [-10, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE + 10],
|
||||||
|
outputRange: isMe
|
||||||
|
? [-MAX_SWIPE_DISTANCE, -MAX_SWIPE_DISTANCE, 0, 0]
|
||||||
|
: [0, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
const webLastTouchRef = useRef<{ x: number } | null>(null);
|
||||||
|
|
||||||
|
const handleWebTouchStart = useCallback((e: any) => {
|
||||||
|
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
|
||||||
|
if (touch && touch.pageX !== undefined) {
|
||||||
|
webTouchStartRef.current = { x: touch.pageX, time: Date.now() };
|
||||||
|
webLastTouchRef.current = { x: touch.pageX };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWebTouchMove = useCallback((e: any) => {
|
||||||
|
const start = webTouchStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
|
||||||
|
if (!touch || touch.pageX === undefined) return;
|
||||||
|
const dx = touch.pageX - start.x;
|
||||||
|
const allowed = isMe ? dx < 5 : dx > -5;
|
||||||
|
if (allowed) {
|
||||||
|
webTranslateXRef.setValue(dx);
|
||||||
|
webLastTouchRef.current = { x: touch.pageX };
|
||||||
|
}
|
||||||
|
}, [isMe]);
|
||||||
|
|
||||||
|
const handleWebTouchEnd = useCallback((e: any) => {
|
||||||
|
const start = webTouchStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
webTouchStartRef.current = null;
|
||||||
|
const touch = e.nativeEvent?.changedTouches?.[0] || e.nativeEvent;
|
||||||
|
if (!touch || touch.pageX === undefined) {
|
||||||
|
Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dx = touch.pageX - start.x;
|
||||||
|
const shouldTrigger = isMe
|
||||||
|
? dx < -SWIPE_THRESHOLD
|
||||||
|
: dx > SWIPE_THRESHOLD;
|
||||||
|
|
||||||
|
if (shouldTrigger && !hasTriggeredRef.current) {
|
||||||
|
hasTriggeredRef.current = true;
|
||||||
|
onReply();
|
||||||
|
setTimeout(() => {
|
||||||
|
hasTriggeredRef.current = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start();
|
||||||
|
}, [isMe, onReply]);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.replyIconContainer,
|
||||||
|
isMe ? styles.replyIconLeft : styles.replyIconRight,
|
||||||
|
{ opacity: webIconOpacity },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="reply"
|
||||||
|
size={20}
|
||||||
|
color={colors.primary.main}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
<View
|
||||||
|
data-gesture-area={true}
|
||||||
|
onTouchStart={handleWebTouchStart}
|
||||||
|
onTouchMove={handleWebTouchMove}
|
||||||
|
onTouchEnd={handleWebTouchEnd}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
transform: [{ translateX: webClampedTranslateX }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* 回复图标 - 在消息后面 */}
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.replyIconContainer,
|
styles.replyIconContainer,
|
||||||
isMe ? styles.replyIconLeft : styles.replyIconRight,
|
isMe ? styles.replyIconLeft : styles.replyIconRight,
|
||||||
{
|
{ opacity: iconOpacity },
|
||||||
opacity: iconOpacity,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
@@ -112,8 +200,6 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
|||||||
color={colors.primary.main}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* 可滑动的消息内容 */}
|
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
onGestureEvent={onGestureEvent}
|
onGestureEvent={onGestureEvent}
|
||||||
onHandlerStateChange={onHandlerStateChange}
|
onHandlerStateChange={onHandlerStateChange}
|
||||||
|
|||||||
@@ -586,10 +586,10 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
{/* 用户名和简介 */}
|
{/* 用户名和简介 */}
|
||||||
<View style={styles.userInfo}>
|
<View style={styles.userInfo}>
|
||||||
<Text variant="h2" style={styles.nickname}>
|
<Text variant="h2" style={styles.nickname}>
|
||||||
{nickname || currentUser?.nickname}
|
{nickname || currentUser?.nickname || ''}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="caption" color={colors.text.secondary} style={styles.username}>
|
<Text variant="caption" color={colors.text.secondary} style={styles.username}>
|
||||||
@{currentUser?.username}
|
@{currentUser?.username || ''}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{bio ? (
|
{bio ? (
|
||||||
@@ -624,7 +624,7 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
<View style={styles.metaTag}>
|
<View style={styles.metaTag}>
|
||||||
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
|
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
|
||||||
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
|
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
|
||||||
{website.replace(/^https?:\/\//, '')}
|
{typeof website === 'string' ? website.replace(/^https?:\/\//, '') : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@@ -638,7 +638,7 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
<View style={styles.metaTag}>
|
<View style={styles.metaTag}>
|
||||||
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
|
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
|
||||||
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
|
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
|
||||||
加入于 {new Date(currentUser?.created_at || Date.now()).getFullYear()}年
|
{`加入于 ${new Date(currentUser?.created_at || Date.now()).getFullYear()}年`}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{!isSelf && (
|
{isCurrentUser && !isSelf && (
|
||||||
<Button
|
<Button
|
||||||
title={buttonConfig.title}
|
title={buttonConfig.title}
|
||||||
variant={buttonConfig.variant}
|
variant={buttonConfig.variant}
|
||||||
@@ -230,7 +230,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{!isSelf && (
|
{isCurrentUser && !isSelf && (
|
||||||
<View style={styles.userCardFooter}>
|
<View style={styles.userCardFooter}>
|
||||||
<Button
|
<Button
|
||||||
title={buttonConfig.title}
|
title={buttonConfig.title}
|
||||||
@@ -278,7 +278,9 @@ const FollowListScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}>
|
<Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}>
|
||||||
{type === 'following' ? '你已关注的用户' : '关注你的用户'}
|
{type === 'following'
|
||||||
|
? (isCurrentUser ? '你已关注的用户' : 'TA关注的用户')
|
||||||
|
: (isCurrentUser ? '关注你的用户' : 'TA的粉丝')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -298,7 +300,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
// 宽屏使用网格布局
|
// 宽屏使用网格布局
|
||||||
if (isWideScreen && columnCount > 1) {
|
if (isWideScreen && columnCount > 1) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<ResponsiveContainer maxWidth={1200}>
|
<ResponsiveContainer maxWidth={1200}>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={users}
|
data={users}
|
||||||
@@ -333,7 +335,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 移动端使用列表布局
|
// 移动端使用列表布局
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={users}
|
data={users}
|
||||||
renderItem={renderUserListItem}
|
renderItem={renderUserListItem}
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles
|
|||||||
|
|
||||||
interface UserProfileScreenProps {
|
interface UserProfileScreenProps {
|
||||||
mode: ProfileMode;
|
mode: ProfileMode;
|
||||||
userId?: string; // 仅 other 模式需要
|
userId?: string;
|
||||||
|
hasHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => {
|
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, hasHeader = false }) => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
|
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
|
||||||
const { isDesktop, isTablet } = useResponsive();
|
const { isDesktop, isTablet } = useResponsive();
|
||||||
@@ -148,7 +149,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
// 未登录/用户不存在状态
|
// 未登录/用户不存在状态
|
||||||
if (mode === 'self' && !currentUser) {
|
if (mode === 'self' && !currentUser) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={sharedStyles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="未登录"
|
title="未登录"
|
||||||
description="请先登录"
|
description="请先登录"
|
||||||
@@ -170,10 +171,12 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const safeAreaEdges = hasHeader ? ['bottom'] : (mode === 'self' ? ['top', 'bottom'] : ['bottom']);
|
||||||
|
|
||||||
// 桌面端使用双栏布局
|
// 桌面端使用双栏布局
|
||||||
if (isDesktop || isTablet) {
|
if (isDesktop || isTablet) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}>
|
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
||||||
<ResponsiveContainer maxWidth={1400}>
|
<ResponsiveContainer maxWidth={1400}>
|
||||||
<View style={sharedStyles.desktopContainer}>
|
<View style={sharedStyles.desktopContainer}>
|
||||||
{/* 左侧:用户信息 */}
|
{/* 左侧:用户信息 */}
|
||||||
@@ -211,7 +214,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
|
|
||||||
// 移动端使用单栏布局
|
// 移动端使用单栏布局
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}>
|
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const UserScreen: React.FC = () => {
|
|||||||
<UserProfileScreen
|
<UserProfileScreen
|
||||||
mode={isSelfProfile ? 'self' : 'other'}
|
mode={isSelfProfile ? 'self' : 'other'}
|
||||||
userId={isSelfProfile ? undefined : userId}
|
userId={isSelfProfile ? undefined : userId}
|
||||||
|
hasHeader
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,6 +302,30 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
[weeks.length]
|
[weeks.length]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const webTouchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
|
||||||
|
const handleWebTouchStart = useCallback((e: any) => {
|
||||||
|
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
|
||||||
|
if (touch && touch.pageX !== undefined) {
|
||||||
|
webTouchStartRef.current = { x: touch.pageX, y: touch.pageY, time: Date.now() };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const handleWebTouchEnd = useCallback((e: any) => {
|
||||||
|
const start = webTouchStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
webTouchStartRef.current = null;
|
||||||
|
const touch = e.nativeEvent?.changedTouches?.[0] || e.nativeEvent;
|
||||||
|
if (!touch || touch.pageX === undefined) return;
|
||||||
|
const dx = touch.pageX - start.x;
|
||||||
|
const dy = touch.pageY - start.y;
|
||||||
|
if (Math.abs(dx) < SWIPE_THRESHOLD) return;
|
||||||
|
if (Math.abs(dx) <= Math.abs(dy) * 0.8) return;
|
||||||
|
if (dx < 0) {
|
||||||
|
setCurrentWeek(prev => Math.min(prev + 1, weeks.length));
|
||||||
|
} else {
|
||||||
|
setCurrentWeek(prev => Math.max(prev - 1, 1));
|
||||||
|
}
|
||||||
|
}, [weeks.length]);
|
||||||
|
|
||||||
const isSlotOccupied = useCallback(
|
const isSlotOccupied = useCallback(
|
||||||
(dayOfWeek: number, mergedSection: number, weekCourses: Course[]) => {
|
(dayOfWeek: number, mergedSection: number, weekCourses: Course[]) => {
|
||||||
return weekCourses.some(course => {
|
return weekCourses.some(course => {
|
||||||
@@ -713,16 +737,20 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 渲染课程表主体
|
// 渲染课程表主体
|
||||||
const renderScheduleBody = () => {
|
const renderScheduleBody = () => {
|
||||||
// 移动端和电脑端都不添加额外底部间距,让内容填满整个区域
|
const gestureLayerProps = Platform.OS === 'web'
|
||||||
// 动态计算的高度已经确保第六节课显示在正确位置
|
? {
|
||||||
|
onTouchStart: handleWebTouchStart,
|
||||||
|
onTouchEnd: handleWebTouchEnd,
|
||||||
|
'data-gesture-area': true,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanGestureHandler
|
Platform.OS === 'web' ? (
|
||||||
activeOffsetX={[-8, 8]}
|
<View
|
||||||
failOffsetY={[-80, 80]}
|
style={styles.scheduleBodyGestureLayer}
|
||||||
onHandlerStateChange={handleWeekSwipe}
|
{...gestureLayerProps}
|
||||||
>
|
>
|
||||||
<View style={styles.scheduleBodyGestureLayer}>
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scheduleBody}
|
style={styles.scheduleBody}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
@@ -732,6 +760,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
nestedScrollEnabled
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
>
|
>
|
||||||
<View style={styles.daysContainer}>
|
<View style={styles.daysContainer}>
|
||||||
@@ -742,7 +771,35 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</PanGestureHandler>
|
) : (
|
||||||
|
<PanGestureHandler
|
||||||
|
activeOffsetX={[-8, 8]}
|
||||||
|
failOffsetY={[-80, 80]}
|
||||||
|
onHandlerStateChange={handleWeekSwipe}
|
||||||
|
>
|
||||||
|
<View style={styles.scheduleBodyGestureLayer}>
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scheduleBody}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.scheduleBodyContent}
|
||||||
|
>
|
||||||
|
{renderTimeColumn()}
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
nestedScrollEnabled
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
<View style={styles.daysContainer}>
|
||||||
|
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
||||||
|
renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1)
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</PanGestureHandler>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user