diff --git a/MeAgent/android/.gitignore b/MeAgent/android/.gitignore new file mode 100644 index 00000000..8a6be077 --- /dev/null +++ b/MeAgent/android/.gitignore @@ -0,0 +1,16 @@ +# OSX +# +.DS_Store + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ + +# Bundle artifacts +*.jsbundle diff --git a/MeAgent/android/app/build.gradle b/MeAgent/android/app/build.gradle new file mode 100644 index 00000000..b1c85bbf --- /dev/null +++ b/MeAgent/android/app/build.gradle @@ -0,0 +1,201 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() + +static def versionToNumber(major, minor, patch) { + return patch * 100 + minor * 10000 + major * 1000000 +} + +def getRNVersion() { + def version = providers.exec { + workingDir(projectDir) + commandLine("node", "-e", "console.log(require('react-native/package.json').version);") + }.standardOutput.asText.get().trim() + + def coreVersion = version.split("-")[0] + def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() } + + return versionToNumber( + major, + minor, + patch + ) +} +def rnVersion = getRNVersion() + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + // Use Expo CLI to bundle the app, this ensures the Metro config + // works correctly with Expo projects. + cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '..' + // root = file("../") + // The folder where the react-native NPM package is. Default is ../node_modules/react-native + // reactNativeDir = file("../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + if (rnVersion >= versionToNumber(0, 75, 0)) { + /* Autolinking */ + autolinkLibrariesWithApp() + } +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +android { + ndkVersion rootProject.ext.ndkVersion + + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace 'com.valuefrontier.meagent' + defaultConfig { + applicationId 'com.valuefrontier.meagent' + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) + } + } + packagingOptions { + jniLibs { + useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + } + } +} + +// Apply static values from `gradle.properties` to the `android.packagingOptions` +// Accepts values in comma delimited lists, example: +// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini +["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> + // Split option: 'foo,bar' -> ['foo', 'bar'] + def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); + // Trim all elements in place. + for (i in 0.. 0) { + println "android.packagingOptions.$prop += $options ($options.length)" + // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' + options.each { + android.packagingOptions[prop] += it + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; + def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; + def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + + if (isGifEnabled) { + // For animated gif support + implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") + } + + if (isWebpEnabled) { + // For webp support + implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") + if (isWebpAnimatedEnabled) { + // Animated webp support + implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") + } + } + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} + +if (rnVersion < versionToNumber(0, 75, 0)) { + apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); + applyNativeModulesAppBuildGradle(project) +} diff --git a/MeAgent/android/app/debug.keystore b/MeAgent/android/app/debug.keystore new file mode 100644 index 00000000..364e105e Binary files /dev/null and b/MeAgent/android/app/debug.keystore differ diff --git a/MeAgent/android/app/proguard-rules.pro b/MeAgent/android/app/proguard-rules.pro new file mode 100644 index 00000000..551eb41d --- /dev/null +++ b/MeAgent/android/app/proguard-rules.pro @@ -0,0 +1,14 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# react-native-reanimated +-keep class com.swmansion.reanimated.** { *; } +-keep class com.facebook.react.turbomodule.** { *; } + +# Add any project specific keep options here: diff --git a/MeAgent/android/app/src/debug/AndroidManifest.xml b/MeAgent/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..3ec2507b --- /dev/null +++ b/MeAgent/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/MeAgent/android/app/src/main/AndroidManifest.xml b/MeAgent/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f7e536ad --- /dev/null +++ b/MeAgent/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainActivity.kt b/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainActivity.kt new file mode 100644 index 00000000..1407529c --- /dev/null +++ b/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainActivity.kt @@ -0,0 +1,61 @@ +package com.valuefrontier.meagent + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainApplication.kt b/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainApplication.kt new file mode 100644 index 00000000..6b11396d --- /dev/null +++ b/MeAgent/android/app/src/main/java/com/valuefrontier/meagent/MainApplication.kt @@ -0,0 +1,55 @@ +package com.valuefrontier.meagent + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List { + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return PackageList(this).packages + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, false) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/MeAgent/android/app/src/main/res/drawable-hdpi/notification_icon.png b/MeAgent/android/app/src/main/res/drawable-hdpi/notification_icon.png new file mode 100644 index 00000000..fa161354 Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-hdpi/notification_icon.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-hdpi/splashscreen_image.png b/MeAgent/android/app/src/main/res/drawable-hdpi/splashscreen_image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-hdpi/splashscreen_image.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-mdpi/notification_icon.png b/MeAgent/android/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 00000000..a49c7157 Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-mdpi/splashscreen_image.png b/MeAgent/android/app/src/main/res/drawable-mdpi/splashscreen_image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-mdpi/splashscreen_image.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xhdpi/notification_icon.png b/MeAgent/android/app/src/main/res/drawable-xhdpi/notification_icon.png new file mode 100644 index 00000000..39cd695c Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xhdpi/notification_icon.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png b/MeAgent/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xxhdpi/notification_icon.png b/MeAgent/android/app/src/main/res/drawable-xxhdpi/notification_icon.png new file mode 100644 index 00000000..4908cc91 Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xxhdpi/notification_icon.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png b/MeAgent/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png b/MeAgent/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png new file mode 100644 index 00000000..b9e4d4f2 Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png differ diff --git a/MeAgent/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png b/MeAgent/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png differ diff --git a/MeAgent/android/app/src/main/res/drawable/rn_edit_text_material.xml b/MeAgent/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 00000000..5c25e728 --- /dev/null +++ b/MeAgent/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/MeAgent/android/app/src/main/res/drawable/splashscreen.xml b/MeAgent/android/app/src/main/res/drawable/splashscreen.xml new file mode 100644 index 00000000..c8568e16 --- /dev/null +++ b/MeAgent/android/app/src/main/res/drawable/splashscreen.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..3941bea9 --- /dev/null +++ b/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..3941bea9 --- /dev/null +++ b/MeAgent/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..6cd8531d Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..6cd8531d Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..424f09a1 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..16ded18e Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..16ded18e Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..54c0faa0 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..30a70f4b Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..30a70f4b Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1a0b9752 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..0ee4dc1c Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..0ee4dc1c Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..cd92afec Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..d85118d4 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..d85118d4 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..0f0d48b4 Binary files /dev/null and b/MeAgent/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/MeAgent/android/app/src/main/res/values-night/colors.xml b/MeAgent/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..3c05de5b --- /dev/null +++ b/MeAgent/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/values/colors.xml b/MeAgent/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..90c5adf9 --- /dev/null +++ b/MeAgent/android/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + #000000 + #000000 + #023c69 + #000000 + #D4AF37 + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/values/strings.xml b/MeAgent/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..fe006ad2 --- /dev/null +++ b/MeAgent/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + 价值前沿 + contain + false + \ No newline at end of file diff --git a/MeAgent/android/app/src/main/res/values/styles.xml b/MeAgent/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..912e38b9 --- /dev/null +++ b/MeAgent/android/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/MeAgent/android/build.gradle b/MeAgent/android/build.gradle new file mode 100644 index 00000000..932bf7b3 --- /dev/null +++ b/MeAgent/android/build.gradle @@ -0,0 +1,41 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' + minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') + compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') + targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') + kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23' + + ndkVersion = "26.1.10909125" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath('com.android.tools.build:gradle') + classpath('com.facebook.react:react-native-gradle-plugin') + classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') + } +} + +apply plugin: "com.facebook.react.rootproject" + +allprojects { + repositories { + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) + } + maven { + // Android JSC is installed from npm + url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) + } + + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + } +} diff --git a/MeAgent/android/gradle.properties b/MeAgent/android/gradle.properties new file mode 100644 index 00000000..40220def --- /dev/null +++ b/MeAgent/android/gradle.properties @@ -0,0 +1,59 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Enable AAPT2 PNG crunching +android.enablePngCrunchInReleaseBuilds=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=false + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Enable GIF support in React Native images (~200 B increase) +expo.gif.enabled=true +# Enable webp support in React Native images (~85 KB increase) +expo.webp.enabled=true +# Enable animated webp support (~3.4 MB increase) +# Disabled by default because iOS doesn't support animated webp +expo.webp.animated=false + +# Enable network inspector +EX_DEV_CLIENT_NETWORK_INSPECTOR=true + +# Use legacy packaging to compress native libraries in the resulting APK. +expo.useLegacyPackaging=false diff --git a/MeAgent/android/gradle/wrapper/gradle-wrapper.jar b/MeAgent/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e6441136 Binary files /dev/null and b/MeAgent/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/MeAgent/android/gradle/wrapper/gradle-wrapper.properties b/MeAgent/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..6f7a6eb3 --- /dev/null +++ b/MeAgent/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/MeAgent/android/gradlew b/MeAgent/android/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/MeAgent/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/MeAgent/android/gradlew.bat b/MeAgent/android/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/MeAgent/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/MeAgent/android/react-settings-plugin/build.gradle.kts b/MeAgent/android/react-settings-plugin/build.gradle.kts new file mode 100644 index 00000000..b4f6668e --- /dev/null +++ b/MeAgent/android/react-settings-plugin/build.gradle.kts @@ -0,0 +1,19 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.9.24" + id("java-gradle-plugin") +} + +repositories { + mavenCentral() +} + +gradlePlugin { + plugins { + create("reactSettingsPlugin") { + id = "com.facebook.react.settings" + implementationClass = "expo.plugins.ReactSettingsPlugin" + } + } +} diff --git a/MeAgent/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt b/MeAgent/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt new file mode 100644 index 00000000..c54f6c7a --- /dev/null +++ b/MeAgent/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt @@ -0,0 +1,10 @@ +package expo.plugins + +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings + +class ReactSettingsPlugin : Plugin { + override fun apply(settings: Settings) { + // Do nothing, just register the plugin. + } +} diff --git a/MeAgent/android/settings.gradle b/MeAgent/android/settings.gradle new file mode 100644 index 00000000..74e99836 --- /dev/null +++ b/MeAgent/android/settings.gradle @@ -0,0 +1,66 @@ +pluginManagement { + def version = providers.exec { + commandLine("node", "-e", "console.log(require('react-native/package.json').version);") + }.standardOutput.asText.get().trim() + def (_, reactNativeMinor, reactNativePatch) = version.split("-")[0].tokenize('.').collect { it.toInteger() } + + includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString()) + if(reactNativeMinor == 74 && reactNativePatch <= 3){ + includeBuild("react-settings-plugin") + } +} + +plugins { id("com.facebook.react.settings") } + +def getRNMinorVersion() { + def version = providers.exec { + commandLine("node", "-e", "console.log(require('react-native/package.json').version);") + }.standardOutput.asText.get().trim() + + def coreVersion = version.split("-")[0] + def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() } + + return minor +} + +if (getRNMinorVersion() >= 75) { + extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + if (System.getenv('EXPO_UNSTABLE_CORE_AUTOLINKING') == '1') { + println('\u001B[32mUsing expo-modules-autolinking as core autolinking source\u001B[0m') + def command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'android' + ].toList() + ex.autolinkLibrariesFromCommand(command) + } else { + ex.autolinkLibrariesFromCommand() + } + } +} + +rootProject.name = '价值前沿' + +dependencyResolutionManagement { + versionCatalogs { + reactAndroidLibs { + from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) + } + } +} + +apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); +useExpoModules() + +if (getRNMinorVersion() < 75) { + apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); + applyNativeModulesSettingsGradle(settings) +} + +include ':app' +includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) diff --git a/MeAgent/ios/Podfile.lock b/MeAgent/ios/Podfile.lock index 3b144b08..7d8333ac 100644 --- a/MeAgent/ios/Podfile.lock +++ b/MeAgent/ios/Podfile.lock @@ -1289,6 +1289,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNIap (12.15.0): + - React-Core - RNKLineView (1.0.0): - lottie-ios (~> 4.5.0) - React @@ -1418,6 +1420,7 @@ DEPENDENCIES: - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNIap (from `../node_modules/react-native-iap`) - RNKLineView (from `../node_modules/react-native-kline-view`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) @@ -1581,6 +1584,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-masked-view/masked-view" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNIap: + :path: "../node_modules/react-native-iap" RNKLineView: :path: "../node_modules/react-native-kline-view" RNReanimated: @@ -1669,6 +1674,7 @@ SPEC CHECKSUMS: RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 RNGestureHandler: 326e35460fb6c8c64a435d5d739bea90d7ed4e49 + RNIap: cfac9771b7fc4e5012a1aea18f32c0ea1f03ac36 RNKLineView: bb63410106d30c7e3a7967c638ef682f9415b2a2 RNReanimated: def444e044c354f38bb0a5926a8583ba19d944c1 RNScreens: a2d8a2555b4653d7a19706eb172f855657ac30d7 diff --git a/MeAgent/ios/app/Info.plist b/MeAgent/ios/app/Info.plist index 4bef78a4..21f6bf1c 100644 --- a/MeAgent/ios/app/Info.plist +++ b/MeAgent/ios/app/Info.plist @@ -42,6 +42,12 @@ NSAllowsLocalNetworking + NSCameraUsageDescription + Allow $(PRODUCT_NAME) to access your camera + NSMicrophoneUsageDescription + Allow $(PRODUCT_NAME) to access your microphone + NSPhotoLibraryUsageDescription + Allow $(PRODUCT_NAME) to access your photos UIBackgroundModes remote-notification diff --git a/MeAgent/navigation/Menu.js b/MeAgent/navigation/Menu.js index 32b400bb..2ba3610f 100644 --- a/MeAgent/navigation/Menu.js +++ b/MeAgent/navigation/Menu.js @@ -166,7 +166,7 @@ function CustomDrawerContent({ { title: "市场热点", navigateTo: "MarketDrawer", icon: "flame", gradient: ["#F59E0B", "#FBBF24"] }, { title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] }, { title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] }, - { title: "社区论坛", navigateTo: "CommunityDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] }, + { title: "社区论坛", navigateTo: "SocialDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] }, { title: "AI 助手", navigateTo: "AgentDrawer", icon: "sparkles", gradient: ["#8B5CF6", "#EC4899"] }, { title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] }, ]; diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js index d3d7897c..2b9ffccd 100644 --- a/MeAgent/navigation/Screens.js +++ b/MeAgent/navigation/Screens.js @@ -17,9 +17,7 @@ import Fashion from "../screens/Fashion"; import Gallery from "../screens/Gallery"; // screens import Home from "../screens/Home"; -import NotificationsScreen from "../screens/Notifications"; -// Notifications -import PersonalNotifications from "../screens/PersonalNotifications"; +// 旧版通知页面已删除,使用 src/screens/Social/Notifications.js import PrivacyScreen from "../screens/Privacy"; // import Onboarding from "../screens/Onboarding"; import Pro from "../screens/Pro"; @@ -30,7 +28,6 @@ import Register from "../screens/Register"; import Search from "../screens/Search"; // settings import SettingsScreen from "../screens/Settings"; -import SystemNotifications from "../screens/SystemNotifications"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { createDrawerNavigator } from "@react-navigation/drawer"; import { createStackNavigator } from "@react-navigation/stack"; @@ -50,14 +47,14 @@ import WatchlistScreen from "../src/screens/Watchlist/WatchlistScreen"; // 新股票详情页面 import { StockDetailScreen } from "../src/screens/StockDetail"; -// 社区页面 -import CommunityHome from "../src/screens/Community"; -import ChannelDetail from "../src/screens/Community/ChannelDetail"; -import ForumChannel from "../src/screens/Community/ForumChannel"; -import PostDetail from "../src/screens/Community/PostDetail"; -import CreatePost from "../src/screens/Community/CreatePost"; -import CreateChannel from "../src/screens/Community/CreateChannel"; -import MemberList from "../src/screens/Community/MemberList"; +// 社交页面 +import SocialHome from "../src/screens/Social"; +import ChannelDetail from "../src/screens/Social/ChannelDetail"; +import ForumChannel from "../src/screens/Social/ForumChannel"; +import PostDetail from "../src/screens/Social/PostDetail"; +import CreatePost from "../src/screens/Social/CreatePost"; +import CreateChannel from "../src/screens/Social/CreateChannel"; +import MemberList from "../src/screens/Social/MemberList"; // 新个人中心页面 import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile"; @@ -80,42 +77,7 @@ const Stack = createStackNavigator(); const Drawer = createDrawerNavigator(); const Tab = createBottomTabNavigator(); -function NotificationsStack(props) { - return ( - ({ - tabBarIcon: ({ focused, color }) => { - let iconName; - if (route.name === "Personal") { - iconName = "user"; - } else if (route.name === "System") { - iconName = "database"; - } - // You can return any component that you like here! - return ( - - ); - }, - })} - tabBarOptions={{ - activeTintColor: argonTheme.COLORS.PRIMARY, - inactiveTintColor: "gray", - labelStyle: { - fontFamily: "open-sans-regular", - }, - }} - > - - - - ); -} +// 旧版 NotificationsStack 已删除,使用社区通知页面 function ElementsStack(props) { return ( @@ -197,21 +159,6 @@ function SettingsStack(props) { cardStyle: { backgroundColor: "#0F172A" }, }} /> - ( -
- ), - cardStyle: { backgroundColor: "#F8F9FE" }, - }} - /> - ( -
- ), - cardStyle: { backgroundColor: "#F8F9FE" }, - }} - /> ); } @@ -435,8 +367,8 @@ function WatchlistStack(props) { ); } -// 社区导航栈 -function CommunityStack(props) { +// 社交导航栈 +function SocialStack(props) { return ( - ( -
- ), - cardStyle: { backgroundColor: "#FFFFFF" }, - }} - /> ); } @@ -753,21 +670,6 @@ function HomeStack(props) { cardStyle: { backgroundColor: "#F8F9FE" }, }} /> - ( -
- ), - cardStyle: { backgroundColor: "#F8F9FE" }, - }} - /> ); } @@ -832,8 +734,8 @@ function AppStack(props) { }} /> - this.setState({ [switchNumber]: !this.state[switchNumber] }); - - renderItem = ({ item }) => ( - - {item.title} - this.toggleSwitch(item.id)} - value={this.state[item.id]} - /> - - ); - - render() { - const notifications = [ - { title: "Someone mentions me", id: "mentions" }, - { title: "Anyone follows me", id: "follows" }, - { title: "Someone comments me", id: "comments" }, - { title: "A seller follows me", id: "seller" } - ]; - - return ( - - item.id} - renderItem={this.renderItem} - ListHeaderComponent={ - - - Recommended Settings - - - These are the most important settings - - - } - /> - - ); - } -} - -const styles = StyleSheet.create({ - notification: { - paddingVertical: theme.SIZES.BASE / 3 - }, - title: { - paddingTop: theme.SIZES.BASE / 2, - paddingBottom: theme.SIZES.BASE * 1.5 - }, - rows: { - paddingHorizontal: theme.SIZES.BASE, - marginBottom: theme.SIZES.BASE * 1.25 - } -}); diff --git a/MeAgent/screens/PersonalNotifications.js b/MeAgent/screens/PersonalNotifications.js deleted file mode 100644 index ea53c6f1..00000000 --- a/MeAgent/screens/PersonalNotifications.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from "react"; -import { ScrollView, Alert } from "react-native"; -import { Block } from "galio-framework"; -import { Notification } from "../components"; -import { argonTheme } from "../constants"; - -export default class PersonalNotifications extends React.Component { - render() { - return ( - - - - Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')} - /> - Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')} - /> - Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')} - /> - Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')} - /> - - - - - ); - } -} diff --git a/MeAgent/screens/SystemNotifications.js b/MeAgent/screens/SystemNotifications.js deleted file mode 100644 index d7e075aa..00000000 --- a/MeAgent/screens/SystemNotifications.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; -import { StyleSheet, ScrollView, Alert } from "react-native"; -import { Block, Text } from "galio-framework"; -import { Notification } from "../components"; -import { argonTheme } from "../constants"; - -export default class SystemNotifications extends React.Component { - render() { - return ( - - - - - - - Unread notifications - - - - - - - - - - - Read notifications - - - - - - - - - - - - - ); - } -} - -const styles = StyleSheet.create({ - card: { - width: '100%', - backgroundColor: argonTheme.COLORS.WHITE, - marginTop: 25, - borderRadius: 6 - }, - cardHeader: { - paddingTop: 20, - paddingBottom: 20, - paddingLeft: 20, - borderColor: 'rgba(0,0,0,0.2)', - borderBottomWidth: StyleSheet.hairlineWidth - } -}); diff --git a/MeAgent/src/hooks/useCommunitySocket.js b/MeAgent/src/hooks/useSocialSocket.js similarity index 92% rename from MeAgent/src/hooks/useCommunitySocket.js rename to MeAgent/src/hooks/useSocialSocket.js index b9214555..a9abdabe 100644 --- a/MeAgent/src/hooks/useCommunitySocket.js +++ b/MeAgent/src/hooks/useSocialSocket.js @@ -1,6 +1,6 @@ /** - * 社区 WebSocket Hook - * 管理社区实时通信连接 + * 社交 WebSocket Hook + * 管理社交实时通信连接 * 与 Web 端保持一致的事件名称和行为 */ @@ -15,7 +15,7 @@ import { addTypingUser, removeTypingUser, incrementUnreadCount, -} from '../store/slices/communitySlice'; +} from '../store/slices/socialSlice'; // 服务器事件类型 - 与 Web 端保持一致(大写) export const SERVER_EVENTS = { @@ -43,10 +43,10 @@ export const CLIENT_EVENTS = { }; /** - * 社区 WebSocket Hook + * 社交 WebSocket Hook * @returns {object} WebSocket 操作方法 */ -export const useCommunitySocket = () => { +export const useSocialSocket = () => { const dispatch = useDispatch(); // 安全获取 auth context,避免在 AuthProvider 外部使用时报错 const authContext = useContext(AuthContext); @@ -88,7 +88,7 @@ export const useCommunitySocket = () => { // 连接成功 socket.on('connect', () => { - console.log('[CommunitySocket] 连接成功'); + console.log('[SocialSocket] 连接成功'); setIsConnected(true); setConnectionError(null); @@ -100,13 +100,13 @@ export const useCommunitySocket = () => { // 连接断开 socket.on('disconnect', (reason) => { - console.log('[CommunitySocket] 连接断开:', reason); + console.log('[SocialSocket] 连接断开:', reason); setIsConnected(false); }); // 连接错误 - 只是警告,不阻断页面 socket.on('connect_error', (error) => { - console.warn('[CommunitySocket] WebSocket 暂不可用,使用离线模式'); + console.warn('[SocialSocket] WebSocket 暂不可用,使用离线模式'); setConnectionError('WebSocket 暂不可用'); }); @@ -186,7 +186,7 @@ export const useCommunitySocket = () => { socketRef.current = socket; } catch (error) { - console.warn('[CommunitySocket] Socket.IO 初始化失败,使用离线模式'); + console.warn('[SocialSocket] Socket.IO 初始化失败,使用离线模式'); setConnectionError('初始化失败'); } }; @@ -212,7 +212,7 @@ export const useCommunitySocket = () => { if (socketRef.current?.connected) { socketRef.current.emit(CLIENT_EVENTS.SUBSCRIBE_CHANNEL, { channelId }); - console.log('[CommunitySocket] 已订阅频道:', channelId); + console.log('[SocialSocket] 已订阅频道:', channelId); } }, []); @@ -224,7 +224,7 @@ export const useCommunitySocket = () => { if (socketRef.current?.connected) { socketRef.current.emit(CLIENT_EVENTS.UNSUBSCRIBE_CHANNEL, { channelId }); - console.log('[CommunitySocket] 已取消订阅频道:', channelId); + console.log('[SocialSocket] 已取消订阅频道:', channelId); } }, []); @@ -298,4 +298,4 @@ export const useCommunitySocket = () => { }; }; -export default useCommunitySocket; +export default useSocialSocket; diff --git a/MeAgent/src/screens/Community/ChannelDetail.js b/MeAgent/src/screens/Social/ChannelDetail.js similarity index 98% rename from MeAgent/src/screens/Community/ChannelDetail.js rename to MeAgent/src/screens/Social/ChannelDetail.js index 59fa4830..e20165c6 100644 --- a/MeAgent/src/screens/Community/ChannelDetail.js +++ b/MeAgent/src/screens/Social/ChannelDetail.js @@ -36,9 +36,9 @@ import { fetchMessages, sendMessage, addMessage, -} from '../../store/slices/communitySlice'; -import { useCommunitySocket } from '../../hooks/useCommunitySocket'; -import { uploadService } from '../../services/communityService'; +} from '../../store/slices/socialSlice'; +import { useSocialSocket } from '../../hooks/useSocialSocket'; +import { uploadService } from '../../services/socialService'; // 消息分组:按日期 const groupMessagesByDate = (messages) => { @@ -162,7 +162,7 @@ const ChannelDetail = ({ route, navigation }) => { const insets = useSafeAreaInsets(); const flatListRef = useRef(null); - const communityState = useSelector((state) => state.community); + const communityState = useSelector((state) => state.social); const messages = communityState?.messages || {}; const messagesHasMore = communityState?.messagesHasMore || {}; const loading = communityState?.loading || {}; @@ -184,7 +184,7 @@ const ChannelDetail = ({ route, navigation }) => { unsubscribe, startTyping, stopTyping, - } = useCommunitySocket(); + } = useSocialSocket(); // 订阅频道 WebSocket useEffect(() => { diff --git a/MeAgent/src/screens/Community/ChannelList.js b/MeAgent/src/screens/Social/ChannelList.js similarity index 98% rename from MeAgent/src/screens/Community/ChannelList.js rename to MeAgent/src/screens/Social/ChannelList.js index 7e7099fc..2280d37a 100644 --- a/MeAgent/src/screens/Community/ChannelList.js +++ b/MeAgent/src/screens/Social/ChannelList.js @@ -24,8 +24,8 @@ import { import { Ionicons } from '@expo/vector-icons'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchChannels, setCurrentChannel } from '../../store/slices/communitySlice'; -import { CHANNEL_TYPES } from '../../services/communityService'; +import { fetchChannels, setCurrentChannel } from '../../store/slices/socialSlice'; +import { CHANNEL_TYPES } from '../../services/socialService'; // 频道图标映射 const CHANNEL_ICONS = { @@ -43,7 +43,7 @@ const CATEGORY_ICONS = { const ChannelList = ({ navigation }) => { const dispatch = useDispatch(); - const communityState = useSelector((state) => state.community); + const communityState = useSelector((state) => state.social); const categories = communityState?.categories || []; const loading = communityState?.loading || {}; diff --git a/MeAgent/src/screens/Community/CreateChannel.js b/MeAgent/src/screens/Social/CreateChannel.js similarity index 98% rename from MeAgent/src/screens/Community/CreateChannel.js rename to MeAgent/src/screens/Social/CreateChannel.js index 44e25adc..4ec9e441 100644 --- a/MeAgent/src/screens/Community/CreateChannel.js +++ b/MeAgent/src/screens/Social/CreateChannel.js @@ -26,8 +26,8 @@ import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useDispatch, useSelector } from 'react-redux'; -import { channelService, CHANNEL_TYPES } from '../../services/communityService'; -import { fetchChannels } from '../../store/slices/communitySlice'; +import { channelService, CHANNEL_TYPES } from '../../services/socialService'; +import { fetchChannels } from '../../store/slices/socialSlice'; // 频道类型选项 const CHANNEL_TYPE_OPTIONS = [ @@ -50,7 +50,7 @@ const CreateChannel = ({ navigation }) => { const insets = useSafeAreaInsets(); // 从 Redux 获取真实的 categories 列表 - const communityState = useSelector((state) => state.community); + const communityState = useSelector((state) => state.social); const categories = communityState?.categories || []; const loadingCategories = communityState?.loading?.channels || false; diff --git a/MeAgent/src/screens/Community/CreatePost.js b/MeAgent/src/screens/Social/CreatePost.js similarity index 99% rename from MeAgent/src/screens/Community/CreatePost.js rename to MeAgent/src/screens/Social/CreatePost.js index 69cc18b7..a32d5086 100644 --- a/MeAgent/src/screens/Community/CreatePost.js +++ b/MeAgent/src/screens/Social/CreatePost.js @@ -31,8 +31,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useDispatch } from 'react-redux'; import * as ImagePicker from 'expo-image-picker'; -import { createPost } from '../../store/slices/communitySlice'; -import { uploadService } from '../../services/communityService'; +import { createPost } from '../../store/slices/socialSlice'; +import { uploadService } from '../../services/socialService'; import { gradients } from '../../theme'; // 预设标签 diff --git a/MeAgent/src/screens/Community/ForumChannel.js b/MeAgent/src/screens/Social/ForumChannel.js similarity index 99% rename from MeAgent/src/screens/Community/ForumChannel.js rename to MeAgent/src/screens/Social/ForumChannel.js index eef9c722..053ad80c 100644 --- a/MeAgent/src/screens/Community/ForumChannel.js +++ b/MeAgent/src/screens/Social/ForumChannel.js @@ -27,7 +27,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchPosts } from '../../store/slices/communitySlice'; +import { fetchPosts } from '../../store/slices/socialSlice'; import { gradients } from '../../theme'; // 排序选项 @@ -69,7 +69,7 @@ const ForumChannel = ({ route, navigation }) => { const dispatch = useDispatch(); const insets = useSafeAreaInsets(); - const { posts, postsHasMore, loading } = useSelector((state) => state.community); + const { posts, postsHasMore, loading } = useSelector((state) => state.social); const channelPosts = posts[channel.id] || []; const hasMore = postsHasMore[channel.id] ?? true; diff --git a/MeAgent/src/screens/Community/MemberList.js b/MeAgent/src/screens/Social/MemberList.js similarity index 98% rename from MeAgent/src/screens/Community/MemberList.js rename to MeAgent/src/screens/Social/MemberList.js index 8397e153..8994b873 100644 --- a/MeAgent/src/screens/Community/MemberList.js +++ b/MeAgent/src/screens/Social/MemberList.js @@ -25,7 +25,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchMembers } from '../../store/slices/communitySlice'; +import { fetchMembers } from '../../store/slices/socialSlice'; // 角色配置 const ROLE_CONFIG = { @@ -39,7 +39,7 @@ const MemberList = ({ route, navigation }) => { const dispatch = useDispatch(); const insets = useSafeAreaInsets(); - const { members, loading } = useSelector((state) => state.community); + const { members, loading } = useSelector((state) => state.social); const channelMembers = members[channel.id] || []; const [searchText, setSearchText] = useState(''); diff --git a/MeAgent/src/screens/Community/Notifications.js b/MeAgent/src/screens/Social/Notifications.js similarity index 98% rename from MeAgent/src/screens/Community/Notifications.js rename to MeAgent/src/screens/Social/Notifications.js index cd3390e2..3a806586 100644 --- a/MeAgent/src/screens/Community/Notifications.js +++ b/MeAgent/src/screens/Social/Notifications.js @@ -28,7 +28,7 @@ import { fetchNotifications, markNotificationRead, markAllNotificationsRead, -} from '../../store/slices/communitySlice'; +} from '../../store/slices/socialSlice'; // 通知类型配置 const NOTIFICATION_CONFIG = { @@ -83,7 +83,7 @@ const Notifications = ({ navigation }) => { const dispatch = useDispatch(); const insets = useSafeAreaInsets(); - const communityState = useSelector((state) => state.community); + const communityState = useSelector((state) => state.social); const notifications = communityState?.notifications || []; const notificationsHasMore = communityState?.notificationsHasMore ?? true; const loading = communityState?.loading || {}; diff --git a/MeAgent/src/screens/Community/PostDetail.js b/MeAgent/src/screens/Social/PostDetail.js similarity index 99% rename from MeAgent/src/screens/Community/PostDetail.js rename to MeAgent/src/screens/Social/PostDetail.js index 8efe6701..251b06d0 100644 --- a/MeAgent/src/screens/Community/PostDetail.js +++ b/MeAgent/src/screens/Social/PostDetail.js @@ -35,8 +35,8 @@ import { fetchReplies, createReply, clearCurrentPost, -} from '../../store/slices/communitySlice'; -import { postService } from '../../services/communityService'; +} from '../../store/slices/socialSlice'; +import { postService } from '../../services/socialService'; // 格式化相对时间 const formatRelativeTime = (dateStr) => { @@ -112,7 +112,7 @@ const PostDetail = ({ route, navigation }) => { const dispatch = useDispatch(); const insets = useSafeAreaInsets(); - const { currentPost, replies, loading } = useSelector((state) => state.community); + const { currentPost, replies, loading } = useSelector((state) => state.social); const postReplies = replies[initialPost.id] || []; const [replyText, setReplyText] = useState(''); diff --git a/MeAgent/src/screens/Community/index.js b/MeAgent/src/screens/Social/index.js similarity index 93% rename from MeAgent/src/screens/Community/index.js rename to MeAgent/src/screens/Social/index.js index a6d8827c..3f851911 100644 --- a/MeAgent/src/screens/Community/index.js +++ b/MeAgent/src/screens/Social/index.js @@ -1,6 +1,6 @@ /** - * 社区主入口 - * Discord 风格的社区论坛 + * 社交主入口 + * Discord 风格的社交论坛 */ import React, { useState } from 'react'; @@ -66,11 +66,11 @@ const CustomTabBar = ({ activeTab, onTabChange, unreadCount }) => { ); }; -const CommunityHome = ({ navigation }) => { +const SocialHome = ({ navigation }) => { const insets = useSafeAreaInsets(); const [activeTab, setActiveTab] = useState('channels'); - const communityState = useSelector((state) => state.community); + const communityState = useSelector((state) => state.social); const unreadCount = communityState?.unreadCount || 0; return ( @@ -96,9 +96,9 @@ const CommunityHome = ({ navigation }) => { ); }; -export default CommunityHome; +export default SocialHome; -// 导出所有社区相关页面 +// 导出所有社交相关页面 export { default as ChannelList } from './ChannelList'; export { default as ChannelDetail } from './ChannelDetail'; export { default as ForumChannel } from './ForumChannel'; diff --git a/MeAgent/src/screens/StockDetail/StockDetailScreen.js b/MeAgent/src/screens/StockDetail/StockDetailScreen.js index 33620007..abb5c682 100644 --- a/MeAgent/src/screens/StockDetail/StockDetailScreen.js +++ b/MeAgent/src/screens/StockDetail/StockDetailScreen.js @@ -33,6 +33,7 @@ import { fetchStockDetail, fetchMinuteData, fetchKlineData, + loadStockPage, setChartType, clearCurrentStock, selectCurrentStock, @@ -134,26 +135,28 @@ const StockDetailScreen = () => { } }, [stockCode]); - // 加载股票数据 + // 加载股票数据(并行加载,提升性能) const loadStockData = useCallback(async () => { if (!stockCode) { console.log('[StockDetailScreen] 无股票代码'); return; } - console.log('[StockDetailScreen] 开始加载数据:', { stockCode, stockName, chartType }); + console.log('[StockDetailScreen] 开始并行加载数据:', { stockCode, stockName, chartType }); - // 加载股票详情 - dispatch(fetchStockDetail(stockCode)); - - // 根据当前图表类型加载数据 + // 根据当前图表类型决定加载策略 if (chartType === 'minute') { - dispatch(fetchMinuteData(stockCode)); + // 分时模式:并行加载股票详情和分时数据 + dispatch(loadStockPage(stockCode)); } else { - dispatch(fetchKlineData({ stockCode, type: chartType, eventTime })); + // K线模式:并行加载股票详情和K线数据 + Promise.all([ + dispatch(fetchStockDetail(stockCode)), + dispatch(fetchKlineData({ stockCode, type: chartType, eventTime })), + ]); } - // 异步加载涨幅分析数据 + // 异步加载涨幅分析数据(不阻塞主流程) loadRiseAnalysis(); }, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]); diff --git a/MeAgent/src/screens/Subscription/SubscriptionScreen.js b/MeAgent/src/screens/Subscription/SubscriptionScreen.js index 74419229..07a2b3a2 100644 --- a/MeAgent/src/screens/Subscription/SubscriptionScreen.js +++ b/MeAgent/src/screens/Subscription/SubscriptionScreen.js @@ -10,6 +10,9 @@ import { Alert, Platform, Linking, + Text as RNText, + View as RNView, + ActivityIndicator, } from 'react-native'; import { Box, @@ -19,8 +22,6 @@ import { Icon, Pressable, Spinner, - Button, - Badge, Divider, } from 'native-base'; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; @@ -92,7 +93,6 @@ const CycleSelector = ({ cycles, selectedCycle, onSelect }) => { mb={2} > { borderColor={selectedCycle === cycle.cycleKey ? '#D4AF37' : 'rgba(255, 255, 255, 0.1)'} borderRadius={10} > - - {cycle.label} - - {cycle.discount > 0 && ( - + - - 省{cycle.discount}% + {cycle.label} + + {cycle.discount > 0 && ( + + -{cycle.discount}% - - )} + )} + ))} @@ -141,7 +133,7 @@ const PriceDisplay = ({ plan, selectedCycle }) => { {option.price} /{option.label} - {option.originalPrice && ( + {option.originalPrice > 0 && ( 原价 ¥{option.originalPrice} @@ -189,27 +181,18 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl > {/* 热门标签 */} {plan.popular && ( - + - + 最受欢迎 - + - + )} {/* 卡片标题区域 */} @@ -219,17 +202,17 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl end={{ x: 1, y: 0 }} style={styles.cardHeader} > - - - - + + + + {plan.displayName} - - + + {plan.description} - - - + + + {/* 卡片内容 */} @@ -259,11 +242,11 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl style={styles.subscribeButton} > {loading ? ( - + ) : ( - + {isCurrentPlan ? '续费' : `订阅${plan.displayName}`} - + )} @@ -278,7 +261,7 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl }; // 当前订阅状态卡片 -const CurrentSubscriptionCard = ({ subscription, onManage }) => { +const CurrentSubscriptionCard = ({ subscription }) => { if (!subscription || !subscription.is_active) { return null; } @@ -305,34 +288,29 @@ const CurrentSubscriptionCard = ({ subscription, onManage }) => { borderColor="rgba(212, 175, 55, 0.3)" borderRadius={16} > - - - - - - - - - {planInfo.name} - - - 使用中 - - - {subscription.end_date && ( - - 到期时间: {new Date(subscription.end_date).toLocaleDateString('zh-CN')} - - )} - - - - 管理 - + + + + + + + + {planInfo.name} + + + 使用中 + + + {subscription.end_date ? ( + + 到期时间: {new Date(subscription.end_date).toLocaleDateString('zh-CN')} + + ) : null} + ); @@ -344,8 +322,8 @@ const SubscriptionScreen = () => { const { user, subscription, refreshUser } = useAuth(); const [selectedCycles, setSelectedCycles] = useState({ - pro: 'yearly', - max: 'yearly', + pro: 'quarterly', + max: 'quarterly', }); const [loading, setLoading] = useState(false); const [loadingPlan, setLoadingPlan] = useState(null); @@ -451,12 +429,6 @@ const SubscriptionScreen = () => { } }, [user, navigation, refreshUser]); - // 管理订阅(打开苹果订阅管理页面) - const handleManageSubscription = useCallback(() => { - // iOS 订阅管理页面 URL - Linking.openURL('https://apps.apple.com/account/subscriptions'); - }, []); - // 恢复购买 const handleRestorePurchases = useCallback(async () => { if (Platform.OS !== 'ios') { @@ -521,14 +493,11 @@ const SubscriptionScreen = () => { 订阅管理 - {/* 占位,保持标题居中 */} + {/* 当前订阅状态 */} - + {/* 标题 */} @@ -608,17 +577,49 @@ const styles = StyleSheet.create({ paddingVertical: 16, paddingHorizontal: 20, }, + cardHeaderContent: { + flexDirection: 'row', + alignItems: 'center', + }, + cardHeaderText: { + marginLeft: 12, + }, + planDisplayName: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + }, + planDescription: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 12, + }, + popularBadgeContainer: { + position: 'absolute', + top: 0, + right: 0, + zIndex: 1, + }, popularBadge: { paddingHorizontal: 12, paddingVertical: 4, borderBottomLeftRadius: 10, }, + popularBadgeText: { + color: '#000', + fontSize: 11, + fontWeight: 'bold', + }, subscribeButton: { paddingVertical: 14, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, + subscribeButtonText: { + color: 'white', + fontSize: 16, + fontWeight: 'bold', + }, }); export default SubscriptionScreen; diff --git a/MeAgent/src/services/iapService.js b/MeAgent/src/services/iapService.js index 37faeb59..a6c9f448 100644 --- a/MeAgent/src/services/iapService.js +++ b/MeAgent/src/services/iapService.js @@ -6,8 +6,9 @@ */ import { Platform } from 'react-native'; -import { - initConnection, + +// 安全导入 react-native-iap(模拟器或开发环境可能不可用) +let initConnection, endConnection, getProducts, getSubscriptions, @@ -16,8 +17,38 @@ import { finishTransaction, purchaseUpdatedListener, purchaseErrorListener, - flushFailedPurchasesCachedAsPendingAndroid, -} from 'react-native-iap'; + flushFailedPurchasesCachedAsPendingAndroid; + +let IAP_AVAILABLE = false; + +try { + const RNIap = require('react-native-iap'); + initConnection = RNIap.initConnection; + endConnection = RNIap.endConnection; + getProducts = RNIap.getProducts; + getSubscriptions = RNIap.getSubscriptions; + requestSubscription = RNIap.requestSubscription; + getAvailablePurchases = RNIap.getAvailablePurchases; + finishTransaction = RNIap.finishTransaction; + purchaseUpdatedListener = RNIap.purchaseUpdatedListener; + purchaseErrorListener = RNIap.purchaseErrorListener; + flushFailedPurchasesCachedAsPendingAndroid = RNIap.flushFailedPurchasesCachedAsPendingAndroid; + IAP_AVAILABLE = true; + console.log('[IAPService] react-native-iap 加载成功'); +} catch (error) { + console.warn('[IAPService] react-native-iap 加载失败,IAP 功能不可用:', error.message); + // 提供空函数作为降级 + initConnection = async () => false; + endConnection = async () => {}; + getProducts = async () => []; + getSubscriptions = async () => []; + requestSubscription = async () => { throw new Error('IAP 不可用'); }; + getAvailablePurchases = async () => []; + finishTransaction = async () => {}; + purchaseUpdatedListener = () => ({ remove: () => {} }); + purchaseErrorListener = () => ({ remove: () => {} }); +} + import { API_BASE_URL } from './api'; // 苹果内购产品 ID 配置 @@ -124,6 +155,11 @@ class IAPService { return false; } + if (!IAP_AVAILABLE) { + console.log('[IAPService] IAP 模块不可用,跳过初始化'); + return false; + } + if (this.isInitialized) { console.log('[IAPService] 已经初始化,跳过'); return true; diff --git a/MeAgent/src/services/communityService.js b/MeAgent/src/services/socialService.js similarity index 79% rename from MeAgent/src/services/communityService.js rename to MeAgent/src/services/socialService.js index ab077a88..45f4d6f0 100644 --- a/MeAgent/src/services/communityService.js +++ b/MeAgent/src/services/socialService.js @@ -1,13 +1,13 @@ /** - * 社区服务层 - * 处理频道、消息、帖子、成员等 API 调用 - * 与 Web 端保持一致,使用 ElasticSearch API 读取数据 + * 社交服务层 + * 处理频道、消息、帖子、成员、通知等 API 调用 + * 频道/消息使用 ElasticSearch API,通知使用后端 REST API */ import { apiRequest, API_BASE_URL } from './api'; -// ES API 基础路径 -const ES_API_BASE = `${API_BASE_URL}/api/community/es`; +// ES API 基础路径(用于频道消息等) +const ES_API_BASE = `${API_BASE_URL}/api/social/es`; // 频道类型 export const CHANNEL_TYPES = { @@ -35,13 +35,13 @@ export const channelService = { try { const response = await fetch(`${API_BASE_URL}/api/community/channels`); if (!response.ok) { - console.warn('[CommunityService] 获取频道失败,使用默认数据'); + console.warn('[SocialService] 获取频道失败,使用默认数据'); return getDefaultChannels(); } const data = await response.json(); return { categories: data.data || [] }; } catch (error) { - console.warn('[CommunityService] getChannels: 使用默认数据'); + console.warn('[SocialService] getChannels: 使用默认数据'); return getDefaultChannels(); } }, @@ -56,7 +56,7 @@ export const channelService = { const data = await response.json(); return data; } catch (error) { - console.warn('[CommunityService] getChannelDetail 错误:', error); + console.warn('[SocialService] getChannelDetail 错误:', error); throw error; } }, @@ -72,7 +72,7 @@ export const channelService = { }); if (!response.ok) throw new Error('订阅频道失败'); } catch (error) { - console.warn('[CommunityService] subscribeChannel 错误:', error); + console.warn('[SocialService] subscribeChannel 错误:', error); throw error; } }, @@ -88,7 +88,7 @@ export const channelService = { }); if (!response.ok) throw new Error('取消订阅失败'); } catch (error) { - console.warn('[CommunityService] unsubscribeChannel 错误:', error); + console.warn('[SocialService] unsubscribeChannel 错误:', error); throw error; } }, @@ -112,7 +112,7 @@ export const channelService = { const result = await response.json(); return result.data; } catch (error) { - console.warn('[CommunityService] createChannel 错误:', error); + console.warn('[SocialService] createChannel 错误:', error); throw error; } }, @@ -159,7 +159,7 @@ export const messageService = { }); if (!response.ok) { - console.warn('[CommunityService] getMessages 失败'); + console.warn('[SocialService] getMessages 失败'); return { data: [], hasMore: false }; } @@ -193,7 +193,7 @@ export const messageService = { hasMore: data.hits.total.value > items.length, }; } catch (error) { - console.warn('[CommunityService] getMessages 错误:', error); + console.warn('[SocialService] getMessages 错误:', error); return { data: [], hasMore: false }; } }, @@ -214,7 +214,7 @@ export const messageService = { const result = await response.json(); return { data: result.data }; } catch (error) { - console.warn('[CommunityService] sendMessage 错误:', error); + console.warn('[SocialService] sendMessage 错误:', error); throw error; } }, @@ -230,7 +230,7 @@ export const messageService = { }); if (!response.ok) throw new Error('删除消息失败'); } catch (error) { - console.warn('[CommunityService] deleteMessage 错误:', error); + console.warn('[SocialService] deleteMessage 错误:', error); throw error; } }, @@ -248,7 +248,7 @@ export const messageService = { }); if (!response.ok) throw new Error('添加表情失败'); } catch (error) { - console.warn('[CommunityService] addReaction 错误:', error); + console.warn('[SocialService] addReaction 错误:', error); throw error; } }, @@ -296,7 +296,7 @@ export const postService = { }); if (!response.ok) { - console.warn('[CommunityService] getPosts 失败'); + console.warn('[SocialService] getPosts 失败'); return { data: [], hasMore: false }; } @@ -329,7 +329,7 @@ export const postService = { hasMore: page * limit < data.hits.total.value, }; } catch (error) { - console.warn('[CommunityService] getPosts 错误:', error); + console.warn('[SocialService] getPosts 错误:', error); return { data: [], hasMore: false }; } }, @@ -344,7 +344,7 @@ export const postService = { const result = await response.json(); return { data: result.data }; } catch (error) { - console.warn('[CommunityService] getPostDetail 错误:', error); + console.warn('[SocialService] getPostDetail 错误:', error); throw error; } }, @@ -365,7 +365,7 @@ export const postService = { const result = await response.json(); return { data: result.data }; } catch (error) { - console.warn('[CommunityService] createPost 错误:', error); + console.warn('[SocialService] createPost 错误:', error); throw error; } }, @@ -398,7 +398,7 @@ export const postService = { }); if (!response.ok) { - console.warn('[CommunityService] getReplies 失败'); + console.warn('[SocialService] getReplies 失败'); return { data: [], hasMore: false }; } @@ -427,7 +427,7 @@ export const postService = { hasMore: page * limit < data.hits.total.value, }; } catch (error) { - console.warn('[CommunityService] getReplies 错误:', error); + console.warn('[SocialService] getReplies 错误:', error); return { data: [], hasMore: false }; } }, @@ -448,7 +448,7 @@ export const postService = { const result = await response.json(); return { data: result.data }; } catch (error) { - console.warn('[CommunityService] createReply 错误:', error); + console.warn('[SocialService] createReply 错误:', error); throw error; } }, @@ -464,7 +464,7 @@ export const postService = { }); if (!response.ok) throw new Error('点赞失败'); } catch (error) { - console.warn('[CommunityService] likePost 错误:', error); + console.warn('[SocialService] likePost 错误:', error); throw error; } }, @@ -501,7 +501,7 @@ export const memberService = { const data = await response.json(); return { data: data.data || [] }; } catch (error) { - console.warn('[CommunityService] getMembers 错误:', error); + console.warn('[SocialService] getMembers 错误:', error); return { data: [] }; } }, @@ -545,43 +545,124 @@ export const uploadService = { const result = await response.json(); return result.data || result; } catch (error) { - console.warn('[CommunityService] uploadImage 错误:', error); + console.warn('[SocialService] uploadImage 错误:', error); throw error; } }, }; /** - * 通知服务 + * 通知服务 - 使用后端 API */ export const notificationService = { /** * 获取通知列表 + * @param {object} options - 分页和筛选选项 + * @param {number} options.page - 页码,默认 1 + * @param {number} options.limit - 每页数量,默认 20 + * @param {boolean} options.unreadOnly - 是否只显示未读,默认 false + * @returns {Promise<{data: Array, hasMore: boolean, unreadCount: number}>} */ getNotifications: async (options = {}) => { - // TODO: 实现通知 API - return { data: [], hasMore: false }; + try { + const { page = 1, limit = 20, unreadOnly = false } = options; + + const params = new URLSearchParams({ + page: String(page), + limit: String(limit), + unread_only: String(unreadOnly), + }); + + const response = await fetch(`${API_BASE_URL}/api/social/notifications?${params}`, { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) { + console.warn('[SocialService] getNotifications 失败,返回空列表'); + return { data: [], hasMore: false, unreadCount: 0 }; + } + + const result = await response.json(); + const data = result.data || {}; + + return { + data: data.notifications || [], + hasMore: data.hasMore || false, + unreadCount: data.unreadCount || 0, + }; + } catch (error) { + console.warn('[SocialService] getNotifications 错误:', error); + return { data: [], hasMore: false, unreadCount: 0 }; + } }, /** * 标记通知为已读 + * @param {string} notificationId - 通知 ID */ markAsRead: async (notificationId) => { - // TODO: 实现标记已读 API + try { + const response = await fetch( + `${API_BASE_URL}/api/social/notifications/${notificationId}/read`, + { + method: 'POST', + credentials: 'include', + } + ); + + if (!response.ok) { + throw new Error('标记已读失败'); + } + } catch (error) { + console.warn('[SocialService] markAsRead 错误:', error); + throw error; + } }, /** * 标记所有通知为已读 */ markAllAsRead: async () => { - // TODO: 实现标记全部已读 API + try { + const response = await fetch( + `${API_BASE_URL}/api/social/notifications/read-all`, + { + method: 'POST', + credentials: 'include', + } + ); + + if (!response.ok) { + throw new Error('标记全部已读失败'); + } + } catch (error) { + console.warn('[SocialService] markAllAsRead 错误:', error); + throw error; + } }, /** * 获取未读通知数量 + * @returns {Promise} */ getUnreadCount: async () => { - return 0; + try { + const response = await fetch(`${API_BASE_URL}/api/social/notifications/unread-count`, { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) { + return 0; + } + + const result = await response.json(); + return result.data?.count || 0; + } catch (error) { + console.warn('[SocialService] getUnreadCount 错误:', error); + return 0; + } }, }; diff --git a/MeAgent/src/store/index.js b/MeAgent/src/store/index.js index 71b56ea0..ee94dbdc 100644 --- a/MeAgent/src/store/index.js +++ b/MeAgent/src/store/index.js @@ -6,7 +6,7 @@ import { configureStore } from '@reduxjs/toolkit'; import eventsReducer from './slices/eventsSlice'; import watchlistReducer from './slices/watchlistSlice'; import stockReducer from './slices/stockSlice'; -import communityReducer from './slices/communitySlice'; +import socialReducer from './slices/socialSlice'; import agentReducer from './slices/agentSlice'; const store = configureStore({ @@ -14,7 +14,7 @@ const store = configureStore({ events: eventsReducer, watchlist: watchlistReducer, stock: stockReducer, - community: communityReducer, + social: socialReducer, agent: agentReducer, }, middleware: (getDefaultMiddleware) => diff --git a/MeAgent/src/store/slices/communitySlice.js b/MeAgent/src/store/slices/socialSlice.js similarity index 93% rename from MeAgent/src/store/slices/communitySlice.js rename to MeAgent/src/store/slices/socialSlice.js index a3e8642d..e9f436f3 100644 --- a/MeAgent/src/store/slices/communitySlice.js +++ b/MeAgent/src/store/slices/socialSlice.js @@ -1,5 +1,5 @@ /** - * 社区 Redux Slice + * 社交 Redux Slice * 管理频道、消息、帖子、成员、通知状态 */ @@ -10,13 +10,13 @@ import { postService, memberService, notificationService, -} from '../../services/communityService'; +} from '../../services/socialService'; // ============ Async Thunks ============ // 获取频道列表 export const fetchChannels = createAsyncThunk( - 'community/fetchChannels', + 'social/fetchChannels', async (_, { rejectWithValue }) => { try { const data = await channelService.getChannels(); @@ -29,7 +29,7 @@ export const fetchChannels = createAsyncThunk( // 获取频道消息 export const fetchMessages = createAsyncThunk( - 'community/fetchMessages', + 'social/fetchMessages', async ({ channelId, options = {} }, { rejectWithValue }) => { try { const response = await messageService.getMessages(channelId, options); @@ -42,7 +42,7 @@ export const fetchMessages = createAsyncThunk( // 发送消息 export const sendMessage = createAsyncThunk( - 'community/sendMessage', + 'social/sendMessage', async ({ channelId, data }, { rejectWithValue }) => { try { const response = await messageService.sendMessage(channelId, data); @@ -55,7 +55,7 @@ export const sendMessage = createAsyncThunk( // 获取帖子列表 export const fetchPosts = createAsyncThunk( - 'community/fetchPosts', + 'social/fetchPosts', async ({ channelId, options = {} }, { rejectWithValue }) => { try { const response = await postService.getPosts(channelId, options); @@ -68,7 +68,7 @@ export const fetchPosts = createAsyncThunk( // 获取帖子详情 export const fetchPostDetail = createAsyncThunk( - 'community/fetchPostDetail', + 'social/fetchPostDetail', async (postId, { rejectWithValue }) => { try { const response = await postService.getPostDetail(postId); @@ -81,7 +81,7 @@ export const fetchPostDetail = createAsyncThunk( // 创建帖子 export const createPost = createAsyncThunk( - 'community/createPost', + 'social/createPost', async ({ channelId, data }, { rejectWithValue }) => { try { const response = await postService.createPost(channelId, data); @@ -94,7 +94,7 @@ export const createPost = createAsyncThunk( // 获取帖子回复 export const fetchReplies = createAsyncThunk( - 'community/fetchReplies', + 'social/fetchReplies', async ({ postId, options = {} }, { rejectWithValue }) => { try { const response = await postService.getReplies(postId, options); @@ -107,7 +107,7 @@ export const fetchReplies = createAsyncThunk( // 创建回复 export const createReply = createAsyncThunk( - 'community/createReply', + 'social/createReply', async ({ postId, data }, { rejectWithValue }) => { try { const response = await postService.createReply(postId, data); @@ -120,7 +120,7 @@ export const createReply = createAsyncThunk( // 获取成员列表 export const fetchMembers = createAsyncThunk( - 'community/fetchMembers', + 'social/fetchMembers', async ({ channelId, options = {} }, { rejectWithValue }) => { try { const response = await memberService.getMembers(channelId, options); @@ -133,11 +133,15 @@ export const fetchMembers = createAsyncThunk( // 获取通知列表 export const fetchNotifications = createAsyncThunk( - 'community/fetchNotifications', + 'social/fetchNotifications', async (options = {}, { rejectWithValue }) => { try { const response = await notificationService.getNotifications(options); - return { notifications: response.data || [], hasMore: response.hasMore }; + return { + notifications: response.data || [], + hasMore: response.hasMore, + unreadCount: response.unreadCount || 0, + }; } catch (error) { return rejectWithValue(error.message); } @@ -146,7 +150,7 @@ export const fetchNotifications = createAsyncThunk( // 标记通知已读 export const markNotificationRead = createAsyncThunk( - 'community/markNotificationRead', + 'social/markNotificationRead', async (notificationId, { rejectWithValue }) => { try { await notificationService.markAsRead(notificationId); @@ -159,7 +163,7 @@ export const markNotificationRead = createAsyncThunk( // 标记所有通知已读 export const markAllNotificationsRead = createAsyncThunk( - 'community/markAllNotificationsRead', + 'social/markAllNotificationsRead', async (_, { rejectWithValue }) => { try { await notificationService.markAllAsRead(); @@ -218,8 +222,8 @@ const initialState = { // ============ Slice ============ -const communitySlice = createSlice({ - name: 'community', +const socialSlice = createSlice({ + name: 'social', initialState, reducers: { // 设置当前频道 @@ -320,7 +324,7 @@ const communitySlice = createSlice({ }, // 重置状态 - resetCommunityState: () => initialState, + resetSocialState: () => initialState, }, extraReducers: (builder) => { builder @@ -462,9 +466,13 @@ const communitySlice = createSlice({ }) .addCase(fetchNotifications.fulfilled, (state, action) => { state.loading.notifications = false; - const { notifications, hasMore } = action.payload; + const { notifications, hasMore, unreadCount } = action.payload; state.notifications = notifications; state.notificationsHasMore = hasMore; + // 更新未读数量(从服务器返回) + if (typeof unreadCount === 'number') { + state.unreadCount = unreadCount; + } }) .addCase(fetchNotifications.rejected, (state, action) => { state.loading.notifications = false; @@ -506,8 +514,8 @@ export const { setUnreadCount, incrementUnreadCount, clearError, - resetCommunityState, -} = communitySlice.actions; + resetSocialState, +} = socialSlice.actions; // 导出 reducer -export default communitySlice.reducer; +export default socialSlice.reducer; diff --git a/app.py b/app.py index fc0e8a9e..9d73bcd7 100755 --- a/app.py +++ b/app.py @@ -1573,6 +1573,52 @@ class UserDeviceToken(db.Model): user = db.relationship('User', backref='device_tokens') +class SocialNotification(db.Model): + """社交通知表 - 用于社区/社交功能的通知""" + __tablename__ = 'social_notifications' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True) + + # 通知类型: reply(回复), mention(提及), like(点赞), follow(关注), system(系统) + type = db.Column(db.String(20), nullable=False, default='system') + + # 发送者信息 + sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + sender_name = db.Column(db.String(100), nullable=True) + sender_avatar = db.Column(db.String(500), nullable=True) + + # 通知内容 + content = db.Column(db.Text, nullable=True) + + # 关联目标: post(帖子), reply(回复), channel(频道), message(消息) + target_type = db.Column(db.String(20), nullable=True) + target_id = db.Column(db.String(50), nullable=True) + + # 状态 + is_read = db.Column(db.Boolean, default=False, index=True) + + created_at = db.Column(db.DateTime, default=beijing_now, index=True) + + # 关系 + user = db.relationship('User', foreign_keys=[user_id], backref='social_notifications') + sender = db.relationship('User', foreign_keys=[sender_id]) + + def to_dict(self): + return { + 'id': str(self.id), + 'type': self.type, + 'senderId': self.sender_id, + 'senderName': self.sender_name, + 'senderAvatar': self.sender_avatar, + 'content': self.content, + 'targetType': self.target_type, + 'targetId': self.target_id, + 'isRead': self.is_read, + 'createdAt': self.created_at.isoformat() if self.created_at else None, + } + + class SubscriptionPlan(db.Model): """订阅套餐表""" __tablename__ = 'subscription_plans' @@ -21201,6 +21247,112 @@ init_prediction_api(db, beijing_now) app.register_blueprint(prediction_bp) +# ==================== 社交通知 API ==================== + +@app.route('/api/social/notifications', methods=['GET']) +@login_required +def get_social_notifications(): + """获取社交通知列表""" + try: + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 20, type=int) + unread_only = request.args.get('unread_only', 'false').lower() == 'true' + + query = SocialNotification.query.filter_by(user_id=current_user.id) + + if unread_only: + query = query.filter_by(is_read=False) + + # 获取总未读数 + unread_count = SocialNotification.query.filter_by( + user_id=current_user.id, + is_read=False + ).count() + + # 分页查询 + total = query.count() + notifications = query.order_by(SocialNotification.created_at.desc()) \ + .offset((page - 1) * limit) \ + .limit(limit) \ + .all() + + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': { + 'notifications': [n.to_dict() for n in notifications], + 'hasMore': page * limit < total, + 'unreadCount': unread_count, + 'total': total, + } + }) + except Exception as e: + app.logger.error(f"获取社交通知失败: {e}") + return jsonify({'code': 500, 'message': str(e)}), 500 + + +@app.route('/api/social/notifications//read', methods=['POST']) +@login_required +def mark_social_notification_read(notification_id): + """标记单个通知为已读""" + try: + notification = SocialNotification.query.filter_by( + id=notification_id, + user_id=current_user.id + ).first() + + if not notification: + return jsonify({'code': 404, 'message': '通知不存在'}), 404 + + notification.is_read = True + db.session.commit() + + return jsonify({'code': 200, 'message': 'success'}) + except Exception as e: + db.session.rollback() + app.logger.error(f"标记通知已读失败: {e}") + return jsonify({'code': 500, 'message': str(e)}), 500 + + +@app.route('/api/social/notifications/read-all', methods=['POST']) +@login_required +def mark_all_social_notifications_read(): + """标记所有通知为已读""" + try: + SocialNotification.query.filter_by( + user_id=current_user.id, + is_read=False + ).update({'is_read': True}) + + db.session.commit() + + return jsonify({'code': 200, 'message': 'success'}) + except Exception as e: + db.session.rollback() + app.logger.error(f"标记所有通知已读失败: {e}") + return jsonify({'code': 500, 'message': str(e)}), 500 + + +@app.route('/api/social/notifications/unread-count', methods=['GET']) +@login_required +def get_social_notifications_unread_count(): + """获取未读通知数量""" + try: + count = SocialNotification.query.filter_by( + user_id=current_user.id, + is_read=False + ).count() + + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': {'count': count} + }) + except Exception as e: + app.logger.error(f"获取未读通知数量失败: {e}") + return jsonify({'code': 500, 'message': str(e)}), 500 + + if __name__ == '__main__': # 创建数据库表 with app.app_context():