From 9a1305f976f6d3cf64735c0eb41f2d007295ec7e Mon Sep 17 00:00:00 2001 From: Myx Date: Fri, 5 Jun 2026 07:40:25 +0200 Subject: [PATCH] feat: Android APP V1 - Experimental Alpha --- .env.example | 12 + .gitea/workflows/build-android-apk.yml | 73 ++ agents-docs/ENGINEERING.md | 1 + agents-docs/FEATURES.md | 1 + agents-docs/features/mobile-capacitor.md | 228 ++++ generate-cert.sh | 10 +- package-lock.json | 991 +++++++++++++++++- package.json | 20 +- server/README.md | 1 + server/src/db/database.ts | 6 +- server/src/entities/DeviceTokenEntity.ts | 25 + server/src/entities/index.ts | 1 + .../migrations/1000000000010-DeviceTokens.ts | 23 + server/src/migrations/index.ts | 4 +- server/src/routes/device-tokens.ts | 58 + server/src/routes/index.ts | 2 + server/src/services/push-dispatch.service.ts | 283 +++++ toju-app/CONTEXT.md | 4 +- toju-app/android/.gitignore | 101 ++ toju-app/android/app/.gitignore | 2 + toju-app/android/app/build.gradle | 54 + toju-app/android/app/capacitor.build.gradle | 25 + .../android/app/google-services.json.example | 29 + toju-app/android/app/proguard-rules.pro | 21 + .../myapp/ExampleInstrumentedTest.java | 26 + .../debug/res/xml/network_security_config.xml | 13 + .../android/app/src/main/AndroidManifest.xml | 59 ++ .../java/com/metoyou/app/MainActivity.java | 13 + .../com/metoyou/app/MetoyouMobilePlugin.java | 115 ++ .../app/VoiceCallForegroundService.java | 76 ++ .../main/res/drawable-land-hdpi/splash.png | Bin 0 -> 7705 bytes .../main/res/drawable-land-mdpi/splash.png | Bin 0 -> 4040 bytes .../main/res/drawable-land-xhdpi/splash.png | Bin 0 -> 9251 bytes .../main/res/drawable-land-xxhdpi/splash.png | Bin 0 -> 13984 bytes .../main/res/drawable-land-xxxhdpi/splash.png | Bin 0 -> 17683 bytes .../main/res/drawable-port-hdpi/splash.png | Bin 0 -> 7934 bytes .../main/res/drawable-port-mdpi/splash.png | Bin 0 -> 4096 bytes .../main/res/drawable-port-xhdpi/splash.png | Bin 0 -> 9875 bytes .../main/res/drawable-port-xxhdpi/splash.png | Bin 0 -> 13346 bytes .../main/res/drawable-port-xxxhdpi/splash.png | Bin 0 -> 17489 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 +++ .../app/src/main/res/drawable/splash.png | Bin 0 -> 4040 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2786 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3450 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4341 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1869 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2110 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2725 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3981 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5036 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6593 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6644 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 9793 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10455 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9441 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 15529 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15916 bytes .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 8 + .../app/src/main/res/values/styles.xml | 22 + .../app/src/main/res/xml/file_paths.xml | 5 + .../main/res/xml/network_security_config.xml | 12 + .../getcapacitor/myapp/ExampleUnitTest.java | 18 + toju-app/android/build.gradle | 29 + toju-app/android/capacitor.settings.gradle | 24 + toju-app/android/gradle.properties | 22 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + toju-app/android/gradlew | 251 +++++ toju-app/android/gradlew.bat | 94 ++ toju-app/android/settings.gradle | 5 + toju-app/android/variables.gradle | 16 + toju-app/capacitor.config.ts | 30 + toju-app/ios/.gitignore | 13 + .../ios/App/App.xcodeproj/project.pbxproj | 376 +++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + toju-app/ios/App/App/AppDelegate.swift | 49 + .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 110522 bytes .../AppIcon.appiconset/Contents.json | 14 + .../ios/App/App/Assets.xcassets/Contents.json | 6 + .../Splash.imageset/Contents.json | 23 + .../Splash.imageset/splash-2732x2732-1.png | Bin 0 -> 41273 bytes .../Splash.imageset/splash-2732x2732-2.png | Bin 0 -> 41273 bytes .../Splash.imageset/splash-2732x2732.png | Bin 0 -> 41273 bytes .../App/Base.lproj/LaunchScreen.storyboard | 32 + .../ios/App/App/Base.lproj/Main.storyboard | 19 + toju-app/ios/App/App/Info.plist | 57 + .../ios/App/App/MetoyouMobilePlugin.swift | 92 ++ toju-app/ios/App/CapApp-SPM/.gitignore | 9 + toju-app/ios/App/CapApp-SPM/Package.swift | 41 + toju-app/ios/App/CapApp-SPM/README.md | 5 + .../Sources/CapApp-SPM/CapApp-SPM.swift | 1 + toju-app/ios/debug.xcconfig | 1 + toju-app/package.json | 19 + toju-app/src/app/app.html | 2 +- toju-app/src/app/app.ts | 11 + .../src/app/core/platform/platform.service.ts | 10 +- .../chat-message-composer.component.html | 187 +++- .../chat-message-composer.component.ts | 65 +- .../composer-media-menu.rules.spec.ts | 87 ++ .../composer-media-menu.rules.ts | 58 + .../custom-emoji-picker.component.html | 45 +- .../custom-emoji-picker.component.spec.ts | 6 + .../custom-emoji-picker.component.ts | 2 + .../services/direct-call.service.spec.ts | 20 + .../services/direct-call.service.ts | 148 ++- .../logic/server-discovery.rules.spec.ts | 19 + .../domain/logic/server-discovery.rules.ts | 16 + .../services/server-directory-api.service.ts | 7 + .../private-call-controls.component.html | 18 + .../private-call-controls.component.ts | 19 +- .../direct-call/private-call.component.html | 3 + .../direct-call/private-call.component.ts | 12 + .../voice-workspace-stream-tile.component.ts | 29 + .../src/app/infrastructure/mobile/README.md | 27 + .../capacitor-mobile-app-lifecycle.adapter.ts | 23 + .../capacitor-mobile-callkit.adapter.ts | 25 + .../capacitor-mobile-media.adapter.ts | 68 ++ .../capacitor-mobile-notifications.adapter.ts | 151 +++ .../capacitor-mobile-persistence.adapter.ts | 35 + ...citor-mobile-picture-in-picture.adapter.ts | 43 + .../capacitor/capacitor-plugin-loader.spec.ts | 75 ++ .../capacitor/capacitor-plugin-loader.ts | 44 + .../capacitor/capacitor-sqlite.store.ts | 115 ++ .../capacitor/metoyou-mobile.plugin.ts | 14 + .../web/web-mobile-app-lifecycle.adapter.ts | 20 + .../web/web-mobile-callkit.adapter.ts | 8 + .../adapters/web/web-mobile-media.adapter.ts | 49 + .../web/web-mobile-notifications.adapter.ts | 60 ++ .../web/web-mobile-persistence.adapter.ts | 10 + .../web-mobile-picture-in-picture.adapter.ts | 26 + .../mobile/contracts/mobile.contracts.ts | 46 + .../src/app/infrastructure/mobile/index.ts | 12 + .../logic/call-notification.rules.spec.ts | 46 + .../mobile/logic/call-notification.rules.ts | 69 ++ .../mobile-push-registration.rules.spec.ts | 64 ++ .../logic/mobile-push-registration.rules.ts | 55 + .../logic/mobile-push-token.rules.spec.ts | 27 + .../mobile/logic/mobile-push-token.rules.ts | 31 + .../logic/mobile-safe-area.rules.spec.ts | 37 + .../mobile/logic/mobile-safe-area.rules.ts | 30 + .../mobile-sqlite-database-name.rules.spec.ts | 17 + .../mobile-sqlite-database-name.rules.ts | 10 + .../logic/mobile-sqlite-execute.rules.ts | 15 + .../mobile-sqlite-row-mapper.rules.spec.ts | 65 ++ .../logic/mobile-sqlite-row-mapper.rules.ts | 320 ++++++ .../logic/mobile-sqlite-schema.rules.spec.ts | 43 + .../logic/mobile-sqlite-schema.rules.ts | 157 +++ .../logic/platform-detection.rules.spec.ts | 61 ++ .../mobile/logic/platform-detection.rules.ts | 45 + .../services/mobile-app-lifecycle.service.ts | 34 + .../services/mobile-call-session.service.ts | 135 +++ .../mobile/services/mobile-callkit.service.ts | 35 + .../mobile/services/mobile-media.service.ts | 42 + .../services/mobile-notifications.service.ts | 56 + .../services/mobile-persistence.service.ts | 29 + .../mobile-picture-in-picture.service.ts | 31 + .../services/mobile-platform.service.spec.ts | 46 + .../services/mobile-platform.service.ts | 51 + .../mobile-push-registration.service.spec.ts | 111 ++ .../mobile-push-registration.service.ts | 139 +++ .../mobile-sqlite-connection.service.ts | 69 ++ .../persistence/capacitor-database.service.ts | 503 +++++++++ .../database-backend.rules.spec.ts | 25 + .../persistence/database-backend.rules.ts | 17 + .../persistence/database.service.spec.ts | 42 +- .../persistence/database.service.ts | 19 +- .../bottom-sheet/bottom-sheet.component.html | 2 +- toju-app/src/index.html | 7 +- toju-app/src/main.ts | 4 + toju-app/src/styles.scss | 22 + tools/build-android-apk.sh | 25 + tools/cap-open-android.js | 45 + tools/resolve-android-studio-path.js | 99 ++ tools/resolve-android-studio-path.test.js | 41 + 179 files changed, 8031 insertions(+), 120 deletions(-) create mode 100644 .gitea/workflows/build-android-apk.yml create mode 100644 agents-docs/features/mobile-capacitor.md create mode 100644 server/src/entities/DeviceTokenEntity.ts create mode 100644 server/src/migrations/1000000000010-DeviceTokens.ts create mode 100644 server/src/routes/device-tokens.ts create mode 100644 server/src/services/push-dispatch.service.ts create mode 100644 toju-app/android/.gitignore create mode 100644 toju-app/android/app/.gitignore create mode 100644 toju-app/android/app/build.gradle create mode 100644 toju-app/android/app/capacitor.build.gradle create mode 100644 toju-app/android/app/google-services.json.example create mode 100644 toju-app/android/app/proguard-rules.pro create mode 100644 toju-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java create mode 100644 toju-app/android/app/src/debug/res/xml/network_security_config.xml create mode 100644 toju-app/android/app/src/main/AndroidManifest.xml create mode 100644 toju-app/android/app/src/main/java/com/metoyou/app/MainActivity.java create mode 100644 toju-app/android/app/src/main/java/com/metoyou/app/MetoyouMobilePlugin.java create mode 100644 toju-app/android/app/src/main/java/com/metoyou/app/VoiceCallForegroundService.java create mode 100644 toju-app/android/app/src/main/res/drawable-land-hdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-land-mdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-land-xhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-port-hdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-port-mdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-port-xhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png create mode 100644 toju-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 toju-app/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 toju-app/android/app/src/main/res/drawable/splash.png create mode 100644 toju-app/android/app/src/main/res/layout/activity_main.xml create mode 100644 toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 toju-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 toju-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 toju-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 toju-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 toju-app/android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 toju-app/android/app/src/main/res/values/strings.xml create mode 100644 toju-app/android/app/src/main/res/values/styles.xml create mode 100644 toju-app/android/app/src/main/res/xml/file_paths.xml create mode 100644 toju-app/android/app/src/main/res/xml/network_security_config.xml create mode 100644 toju-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java create mode 100644 toju-app/android/build.gradle create mode 100644 toju-app/android/capacitor.settings.gradle create mode 100644 toju-app/android/gradle.properties create mode 100644 toju-app/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 toju-app/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 toju-app/android/gradlew create mode 100644 toju-app/android/gradlew.bat create mode 100644 toju-app/android/settings.gradle create mode 100644 toju-app/android/variables.gradle create mode 100644 toju-app/capacitor.config.ts create mode 100644 toju-app/ios/.gitignore create mode 100644 toju-app/ios/App/App.xcodeproj/project.pbxproj create mode 100644 toju-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 toju-app/ios/App/App/AppDelegate.swift create mode 100644 toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 toju-app/ios/App/App/Assets.xcassets/Contents.json create mode 100644 toju-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json create mode 100644 toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png create mode 100644 toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png create mode 100644 toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png create mode 100644 toju-app/ios/App/App/Base.lproj/LaunchScreen.storyboard create mode 100644 toju-app/ios/App/App/Base.lproj/Main.storyboard create mode 100644 toju-app/ios/App/App/Info.plist create mode 100644 toju-app/ios/App/App/MetoyouMobilePlugin.swift create mode 100644 toju-app/ios/App/CapApp-SPM/.gitignore create mode 100644 toju-app/ios/App/CapApp-SPM/Package.swift create mode 100644 toju-app/ios/App/CapApp-SPM/README.md create mode 100644 toju-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift create mode 100644 toju-app/ios/debug.xcconfig create mode 100644 toju-app/package.json create mode 100644 toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/composer-media-menu.rules.spec.ts create mode 100644 toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/composer-media-menu.rules.ts create mode 100644 toju-app/src/app/domains/server-directory/domain/logic/server-discovery.rules.spec.ts create mode 100644 toju-app/src/app/domains/server-directory/domain/logic/server-discovery.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/README.md create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-app-lifecycle.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-callkit.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-media.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-notifications.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-persistence.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-picture-in-picture.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-sqlite.store.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/capacitor/metoyou-mobile.plugin.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-app-lifecycle.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-callkit.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-media.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-notifications.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-persistence.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-picture-in-picture.adapter.ts create mode 100644 toju-app/src/app/infrastructure/mobile/contracts/mobile.contracts.ts create mode 100644 toju-app/src/app/infrastructure/mobile/index.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-execute.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-app-lifecycle.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-call-session.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-callkit.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-media.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-notifications.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-persistence.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-picture-in-picture.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.spec.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.ts create mode 100644 toju-app/src/app/infrastructure/mobile/services/mobile-sqlite-connection.service.ts create mode 100644 toju-app/src/app/infrastructure/persistence/capacitor-database.service.ts create mode 100644 toju-app/src/app/infrastructure/persistence/database-backend.rules.spec.ts create mode 100644 toju-app/src/app/infrastructure/persistence/database-backend.rules.ts create mode 100755 tools/build-android-apk.sh create mode 100644 tools/cap-open-android.js create mode 100644 tools/resolve-android-studio-path.js create mode 100644 tools/resolve-android-studio-path.test.js diff --git a/.env.example b/.env.example index 8d5b43c..32aa19d 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,15 @@ # When false: plain HTTP everywhere (only works on localhost) # Overrides server/data/variables.json for local development only SSL=true + +# --- Mobile push dispatch (signaling server) --- +# Android FCM HTTP v1 (choose one) +# FCM_SERVICE_ACCOUNT_PATH=/absolute/path/to/firebase-service-account.json +# FCM_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"..."} + +# iOS APNs HTTP/2 (.p8 key from Apple Developer) +# APNS_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8 +# APNS_KEY_ID=XXXXXXXXXX +# APNS_TEAM_ID=XXXXXXXXXX +# APNS_BUNDLE_ID=com.metoyou.app +# APNS_USE_SANDBOX=true diff --git a/.gitea/workflows/build-android-apk.yml b/.gitea/workflows/build-android-apk.yml new file mode 100644 index 0000000..89ea573 --- /dev/null +++ b/.gitea/workflows/build-android-apk.yml @@ -0,0 +1,73 @@ +name: Build Android APK + +on: + push: + branches: [main, master] + paths: + - 'toju-app/**' + - 'package.json' + - 'package-lock.json' + - 'tools/build-android-apk.sh' + - '.gitea/workflows/build-android-apk.yml' + workflow_dispatch: + +jobs: + build-android-apk: + runs-on: ubuntu-latest + container: node:22 + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Restore npm cache + uses: https://github.com/actions/cache@v4 + with: + path: /root/.npm + key: npm-android-${{ hashFiles('package-lock.json') }} + restore-keys: npm-android- + + - name: Restore Gradle cache + uses: https://github.com/actions/cache@v4 + with: + path: | + /root/.gradle/caches + /root/.gradle/wrapper + key: gradle-android-${{ hashFiles('toju-app/android/**/*.gradle*', 'toju-app/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: gradle-android- + + - name: Install Android build toolchain + run: | + apt-get update + apt-get install -y --no-install-recommends openjdk-21-jdk wget unzip + + export ANDROID_SDK_ROOT=/opt/android-sdk + mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" + cd /tmp + wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip + unzip -q commandlinetools-linux-11076708_latest.zip + mv cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest" + export PATH="$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools" + + yes | sdkmanager --licenses >/dev/null + sdkmanager "platform-tools" "platforms;android-36" "build-tools;35.0.0" + + echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" + echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" + echo "JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64" >> "$GITHUB_ENV" + echo "PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_ENV" + + - name: Install dependencies + env: + NODE_ENV: development + run: npm ci + + - name: Build debug APK + run: bash tools/build-android-apk.sh + + - name: Upload APK artifact + uses: https://github.com/actions/upload-artifact@v4 + with: + name: metoyou-android-debug-apk + path: toju-app/android/app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error diff --git a/agents-docs/ENGINEERING.md b/agents-docs/ENGINEERING.md index 1a1244f..3ff240d 100644 --- a/agents-docs/ENGINEERING.md +++ b/agents-docs/ENGINEERING.md @@ -124,6 +124,7 @@ Behavioral changes to any of these qualify as a feature-doc update under the rul - `release-draft.yml` — queues release builds on push to `main` / `master` - `publish-draft-release.yml` — publishes draft releases - `deploy-web-apps.yml` — deploys the marketing site and Docusaurus docs + - `build-android-apk.yml` — builds a debug Capacitor Android APK on push (mobile-related paths) or manual dispatch; uploads `app-debug.apk` as a workflow artifact - All checks must pass before merging a PR - Workflow status is visible in the Gitea PR view; use the web UI or `tea` CLI to inspect runs diff --git a/agents-docs/FEATURES.md b/agents-docs/FEATURES.md index b4cf167..67fa312 100644 --- a/agents-docs/FEATURES.md +++ b/agents-docs/FEATURES.md @@ -9,6 +9,7 @@ It must stay accurate as new features are introduced, renamed, merged, or remove ## Feature list (alphabetical) - [Custom Emoji](features/custom-emoji.md) — peer-synced user-created emoji assets, chat reaction shortcuts, and composer emoji insertion. +- [Mobile Capacitor](features/mobile-capacitor.md) — Capacitor native shell, mobile infrastructure facades, and phone-specific call/chat/media integrations. - [Server Discovery](features/server-discovery.md) — featured/trending public-server REST endpoints (server) consumed by the `/dashboard` and `/servers` client pages. - [Signal Server Tag](features/signal-server-tag.md) — configurable signal-server display tag shown on profile cards for a user's registration server. diff --git a/agents-docs/features/mobile-capacitor.md b/agents-docs/features/mobile-capacitor.md new file mode 100644 index 0000000..3a25acc --- /dev/null +++ b/agents-docs/features/mobile-capacitor.md @@ -0,0 +1,228 @@ +# Mobile Capacitor + +Cross-context mobile shell for the Angular product client (`toju-app/`). Wraps the existing SPA in Ionic Capacitor native projects (`toju-app/android/`, `toju-app/ios/`) while keeping Capacitor APIs behind `toju-app/src/app/infrastructure/mobile/`. + +## Responsibilities + +- Detect runtime shell (`browser`, `capacitor`, `electron`) without importing native plugins in domain code. +- Expose facades for notifications, in-call controls, media/attachments, stream pop-out, background audio session, CallKit, and native persistence. +- Integrate with direct-call, voice-workspace, and chat composer flows. + +## Boundaries + +| Layer | Owns | +|-------|------| +| `infrastructure/mobile/` | Platform detection, plugin lazy-loading, web/Capacitor adapters | +| `infrastructure/persistence/` | `DatabaseService` routing (`browser` / `capacitor-sqlite` / `electron`) | +| Domains (`direct-call`, `chat`, `voice-session`) | Business orchestration; inject mobile facades only | +| `core/platform/PlatformService` | Adds `isCapacitor` flag for persistence routing | +| Capacitor native projects | OS permissions, push certificates, store packaging | + +## Build & run + +```bash +# Production web bundle (Capacitor webDir) +npm run build:prod + +# Copy web assets into native projects +npm run cap:sync + +# Open IDE +npm run cap:open:android +npm run cap:open:ios + +### Linux: Android Studio path + +Capacitor defaults to `/usr/local/android-studio/bin/studio.sh`. If Android Studio is installed elsewhere (common with **Flatpak** from Flathub), `npm run cap:open:android` uses `tools/resolve-android-studio-path.js` to locate `studio.sh` (Flatpak `active` symlink, Toolbox, snap, `/opt`, etc.). Override anytime with `CAPACITOR_ANDROID_STUDIO_PATH`. + +# Convenience (build + sync + open) +npm run cap:build:android +npm run cap:build:ios + +# CI / Linux: production web bundle + Capacitor sync + Gradle debug APK +npm run cap:apk:android +# → toju-app/android/app/build/outputs/apk/debug/app-debug.apk +``` + +Config: `toju-app/capacitor.config.ts` (`webDir: ../dist/client/browser`). + +### CI (Gitea) + +Workflow `.gitea/workflows/build-android-apk.yml` runs automatically on push to `main` / `master` when mobile client paths change (`toju-app/**`, root lockfile, or the workflow itself). Use **workflow_dispatch** to build on demand from any branch. + +The job installs JDK 21 and Android SDK platform 36 inside the `node:22` container, runs `tools/build-android-apk.sh`, and uploads `metoyou-android-debug-apk` (`app-debug.apk`) as a workflow artifact. No signing keystore is configured — output is a **debug** APK suitable for sideloading and QA. + +Optional `google-services.json` is not injected in CI; push registration in artifact builds follows the same optional-Firebase behavior as local unsigned debug builds. + +After dependency or plugin changes, run `npm run build:prod && npm run cap:sync` so native projects register `@capacitor/app`, `@capacitor-community/sqlite`, push plugins, and `MetoyouMobile`. + +## Feature status + +| Feature | Status | Notes | +|---------|--------|-------| +| Push/local notifications | **Working (partial)** | Local notifications always available; remote push (FCM/APNs) registers only when Firebase/APNs is configured — app starts normally without `google-services.json` | +| Server push dispatch | **Working (configured)** | Tokens persist in server SQLite; outbound FCM/APNs via env credentials | +| In-call notifications | **Working (Capacitor)** | Persistent notification with answer/mute/hang-up actions | +| Stream pop-out (PiP) | **Working (partial)** | Document PiP when WebView supports it; Android native PiP fallback via `MetoyouMobile` plugin | +| Background voice | **Working (partial)** | Android foreground service; iOS `UIBackgroundModes` audio + CallKit active-call bridge | +| iOS CallKit | **Working (partial)** | `MetoyouMobile.startCallKitSession` reports active calls; requires Xcode target wiring after `cap:sync` | +| Screensharing | **Limited** | Disabled on iOS WebView; Android `getDisplayMedia` may work | +| Composer attachments | **Working** | Mobile attachment button + hidden file input | +| Camera sharing | **Working** | Existing `getUserMedia` camera path in WebRTC stack | +| Speakerphone | **Working (partial)** | Android `AudioManager` via `MetoyouMobile`; iOS `@capgo/capacitor-audio-session`; direct-call speaker toggle on native mobile | +| Local DB (SQLite) | **Working** | `DatabaseService` routes Capacitor shells to `CapacitorDatabaseService` (native SQLite CRUD) | + +## Platform limitations + +- **iOS background WebRTC:** OS may still suspend peer connections when backgrounded despite `audio` background mode and CallKit reporting. +- **iOS CallKit:** Plugin Swift source ships in `ios/App/App/MetoyouMobilePlugin.swift`; add it to the Xcode target if not auto-linked. Incoming-call UI is not fully bridged to WebRTC answer/hang-up yet. +- **iOS screenshare:** `getDisplayMedia` is not available in WKWebView. +- **Android PiP:** Native PiP enters activity-level PiP; WebView video may not always render inside PiP on all OEM WebViews. +- **Production discovery:** `signal.toju.app` may not expose `/api/servers/featured` or `/trending`; client skips those calls for known hosts. +- **Push delivery:** Requires FCM service account and APNs key configuration on the signaling server. + +## Push notification setup (FCM / APNs) + +### Android (FCM) + +The app starts without Firebase. `MobilePushRegistrationService` probes `MetoyouMobile.isRemotePushConfigured()` (Firebase `FirebaseApp` on Android) before calling `PushNotifications.register()`; when unconfigured it logs a single warning and skips registration. + +1. Create a Firebase project and add an Android app with package `com.metoyou.app`. +2. Copy `toju-app/android/app/google-services.json.example` to `google-services.json` (gitignored) and fill in your Firebase values. +3. Run `npm run cap:sync` so the Google Services Gradle plugin applies when the file is present (`build.gradle` applies it only when the JSON exists). +4. Rebuild with `npm run cap:build:android`. +5. Ensure `POST_NOTIFICATIONS`, `RECORD_AUDIO`, and foreground-service permissions are granted on Android 13+. +6. Verify `MobilePushRegistrationService` logs a registration token after login. + +### iOS (APNs) + +1. Enable Push Notifications capability in Xcode for the `App` target. +2. Upload your APNs key/certificate in Apple Developer portal. +3. `Info.plist` includes `remote-notification`, `audio`, and `voip` background modes. +4. Run on a physical device; simulator push registration is limited. + +### Server token storage & dispatch + +Clients POST: + +```http +POST /api/users/device-tokens +Content-Type: application/json + +{ "userId": "", "platform": "android|ios", "token": "" } +``` + +Tokens persist in server SQLite (`device_tokens` table). Outbound push uses repository-root `.env` credentials: + +| Variable | Purpose | +|----------|---------| +| `FCM_SERVICE_ACCOUNT_PATH` or `FCM_SERVICE_ACCOUNT_JSON` | Android FCM HTTP v1 | +| `APNS_KEY_PATH`, `APNS_KEY_ID`, `APNS_TEAM_ID` | iOS APNs HTTP/2 | +| `APNS_BUNDLE_ID` | Defaults to `com.metoyou.app` | +| `APNS_USE_SANDBOX` | `true` for development builds | + +Manual dispatch (ops/testing): + +```http +POST /api/users/device-tokens/:userId/dispatch +{ "title": "Incoming call", "body": "Alice is calling" } +``` + +## Android foreground service + +`VoiceCallForegroundService` starts when `MobileCallSessionService` begins an active call. Required manifest permissions: + +- `FOREGROUND_SERVICE` +- `FOREGROUND_SERVICE_MICROPHONE` +- `RECORD_AUDIO` +- `POST_NOTIFICATIONS` + +The service shows a low-importance ongoing notification while a call is active. + +## SQLite persistence (Capacitor) + +- Schema rules: `infrastructure/mobile/logic/mobile-sqlite-schema.rules.ts` (mirrors Electron entities). +- Statement execution: `infrastructure/mobile/logic/mobile-sqlite-execute.rules.ts` — `@capacitor-community/sqlite` `execute()` accepts **one** SQL statement per call; migrations run each DDL statement separately (never concatenated). +- Row mapping: `infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.ts`. +- CRUD service: `infrastructure/persistence/capacitor-database.service.ts`. +- Routing: `infrastructure/persistence/database-backend.rules.ts` — Capacitor uses SQLite, not IndexedDB. +- Per-user database files: `metoyou__` via `mobile-sqlite-database-name.rules.ts`. +- First launch runs DDL migrations stored in the `meta` table. Schema init failures are cached per database file so the client does not retry in a loop. + +## Capacitor plugin loading + +- `infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.ts` uses **static** `@capacitor/*` imports and `Capacitor.isPluginAvailable()` before returning a plugin. Do not `import()` plugin modules dynamically or `await` plugin objects (Capacitor proxies expose a throwing `.then()` stub). +- After adding or upgrading Capacitor plugins, run `npm run build:prod && npm run cap:sync` so Android/iOS native projects register `App`, `LocalNotifications`, push, and SQLite. + +## Safe area (Android) + +- Capacitor `SystemBars` injects `--safe-area-inset-*` CSS variables into `document.documentElement`. `index.html` sets `viewport-fit=cover` and default inset values; `main.ts` calls `applyMobileSafeAreaDefaults()` so injection never hits a missing root element after the WebView loads. +- `capacitor.config.ts` sets `plugins.SystemBars.insetsHandling: 'css'` so Android WebView versions that mis-report `env(safe-area-inset-*)` still receive correct insets. +- Global `styles.scss` applies inset padding on `html` (with `env()` fallback) and sizes `app-root` to `height: 100%` so content stays below the status bar and above the navigation bar in edge-to-edge mode. + +## Self-hosted HTTPS signal servers (Android) + +Electron and desktop browsers accept the repo's self-signed `.certs/localhost.crt` because Electron runs with `ignore-certificate-errors` when `SSL=true`, and browsers let users bypass the warning once. **Android WebView does neither** — it only trusts system CAs (release) or system + user-installed CAs (debug builds). + +| Runtime | Trust behavior | +|---------|----------------| +| Electron (`SSL=true`) | Ignores certificate errors (`electron/app/flags.ts`) | +| Browser | User accepts warning or imports CA | +| Android debug APK | System CAs + **user-installed CAs** (`src/debug/res/xml/network_security_config.xml`) | +| Android release APK | **System CAs only** — use Let's Encrypt or another public CA | + +### Certificate requirements + +1. **Trust:** Install `.certs/localhost.crt` on the Android device as a **CA certificate** (Settings → Security → Encryption & credentials → Install a certificate → CA certificate). Debug APKs pick this up automatically; release builds ignore user CAs. +2. **SAN:** The cert must list every host clients use. Regenerate with the server IP in the SAN when connecting by IP: + + ```bash + rm -rf .certs + SERVER_IP=46.59.68.77 ./generate-cert.sh + ``` + + Restart the signaling server after regenerating certs. + +3. **HTTPS only:** `AndroidManifest.xml` sets `android:usesCleartextTraffic="false"`. Server URLs must use `https://` (matching `environment.ts` / saved server endpoints). + +### Troubleshooting + +| Symptom | Likely cause | +|---------|----------------| +| `ERR_CERT_AUTHORITY_INVALID` / silent fetch failure | CA not installed on device, or testing a **release** APK with a self-signed cert | +| `ERR_CERT_COMMON_NAME_INVALID` | Cert SAN missing the IP/hostname (regenerate with `SERVER_IP`) | +| `ERR_CONNECTION_REFUSED` | Port unreachable from the phone (firewall, NAT, server not listening on `0.0.0.0`) — verify with `curl -k https://46.59.68.77:3001/api/health` from the device browser first | +| Works in Chrome on phone, fails in app | Chrome may use a different trust store path; ensure the CA is installed at the **system** level, not only per-browser | + +Network security configs: + +- `android/app/src/main/res/xml/network_security_config.xml` — release (system CAs, no cleartext) +- `android/app/src/debug/res/xml/network_security_config.xml` — debug (+ user CAs for dev) + +**Do not commit** `.certs/*.crt`, `.certs/*.key`, or device-specific credential files. + +## Integration points + +- `DirectCallService` — incoming/active call notifications, ring-queue on user hydration, notification action routing. +- `PrivateCallComponent` — speakerphone toggle on native mobile shells. +- `ChatMessageComposerComponent` — `shouldShowAttachmentButton` + `pickAttachmentsFromDevice()`. +- `VoiceWorkspaceStreamTileComponent` — PiP when focused stream tile backgrounds. +- `MobileCallSessionService` — CallKit + foreground service + in-call notifications. +- `App` bootstrap — initializes mobile persistence, lifecycle, call-session, and push registration wiring. + +## Phase 3 completion notes + +Phase 3 delivered: + +1. Full `CapacitorDatabaseService` CRUD with `DatabaseService` routing on `isCapacitor`. +2. Server SQLite persistence for device tokens plus FCM/APNs outbound dispatch. +3. iOS CallKit bridge (partial) via `MetoyouMobile` plugin and `MobileCallKitService`. +4. Android Firebase Gradle wiring with `google-services.json.example` (real file gitignored). +5. Capacitor plugin availability checks to avoid hard failures when plugins are missing pre-sync. +6. Discovery endpoint skip for production signal hosts without featured/trending routes. + +Remaining work: + +- Wire CallKit answer/end actions back into `DirectCallService`. +- Migrate legacy IndexedDB mobile data into SQLite where needed. +- Deploy featured/trending routes to production signal servers or add capability negotiation in health checks. diff --git a/generate-cert.sh b/generate-cert.sh index 42375a3..33b8195 100755 --- a/generate-cert.sh +++ b/generate-cert.sh @@ -15,13 +15,21 @@ fi mkdir -p "$CERT_DIR" +# Optional: include a LAN/public IP in the certificate SAN (required when clients connect by IP). +# Example: SERVER_IP=46.59.68.77 ./generate-cert.sh +SAN="DNS:localhost,IP:127.0.0.1,IP:0.0.0.0" +if [ -n "${SERVER_IP:-}" ]; then + SAN="$SAN,IP:$SERVER_IP" + echo "Including SERVER_IP=$SERVER_IP in certificate SAN." +fi + echo "Generating self-signed certificate..." openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout "$CERT_DIR/localhost.key" \ -out "$CERT_DIR/localhost.crt" \ -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0" + -addext "subjectAltName=$SAN" echo "Done. Certificate written to:" echo " $CERT_DIR/localhost.crt" diff --git a/package-lock.json b/package-lock.json index 6b79486..7b4a957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,15 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "@capacitor-community/sqlite": "^8.1.0", + "@capacitor/app": "^8.1.0", + "@capacitor/camera": "^8.2.0", + "@capacitor/core": "^8.4.0", + "@capacitor/device": "^8.0.2", + "@capacitor/filesystem": "^8.1.2", + "@capacitor/local-notifications": "^8.2.0", + "@capacitor/push-notifications": "^8.1.1", + "@capgo/capacitor-audio-session": "^8.0.40", "@codemirror/commands": "^6.10.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-json": "^6.0.2", @@ -61,6 +70,9 @@ "@angular/build": "^21.0.4", "@angular/cli": "^21.0.4", "@angular/compiler-cli": "^21.0.0", + "@capacitor/android": "^8.4.0", + "@capacitor/cli": "^8.4.0", + "@capacitor/ios": "^8.4.0", "@eslint/js": "^9.39.3", "@playwright/test": "^1.59.1", "@stylistic/eslint-plugin-js": "^4.4.1", @@ -2737,6 +2749,268 @@ "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@capacitor-community/sqlite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-8.1.0.tgz", + "integrity": "sha512-yhKZDAVPDPcM3QE6UGB3LXyV25a6Rve1SjZ1aUpTE0E2isnYTVM0PG9+JOI241f+NdsHzPTE7ESJiYSqKsKnuA==", + "license": "MIT", + "dependencies": { + "jeep-sqlite": "^2.7.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.0.tgz", + "integrity": "sha512-K1ZPkQzvRzPEALz9nBdLx5p5nAPzp5fsTYWk7LRiKZeH/NXqjDvqfTv7lrLgrziQNoDeaL6ijg64oBREzXiV+g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.4.0" + } + }, + "node_modules/@capacitor/app": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz", + "integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/camera": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-8.2.0.tgz", + "integrity": "sha512-hYfrT6xpL936qoEkIpJzSnb0fQCaTkOux1cXzGBfH8QLOGqr6gSLiWZlZz/fqMPmMKJMNRBqlTQkj5fuMhVZog==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.4.0.tgz", + "integrity": "sha512-5Z9RKHxiqJYRTLrfMeZmzR4qrlg5B85MxsWZ5goyXsLkO3bgpW9a1qV/6fR1SX9s5gwLza5y7PZVwITl/hDJ7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@capacitor/cli/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@capacitor/cli/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@capacitor/cli/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@capacitor/cli/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.0.tgz", + "integrity": "sha512-LrS1xPIrqLtJABBIPDGXxxKmI9OyesrzWw8DiHbxhSC9JoiLUleUAJlX1a0LWIVLRbuY4Szgf9huFeRqYH2SAQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/device": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/device/-/device-8.0.2.tgz", + "integrity": "sha512-fIqSXnG0s6bz5A/0xFgSXDkbU+Xl65ti80LhucNvLI4kGhJzcNn6SwWVwpXN9SJTOFWXblXknHNppheP8X1frQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/filesystem": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.2.tgz", + "integrity": "sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.4.0.tgz", + "integrity": "sha512-tnwstEdbTJ2nHAfoAwnurXgYRscWeLY+IIGdz69o24gN2Crfj9Xc0TWo8L5uFLF1LmpbUywH1IT0U1oHV8c+CA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.4.0" + } + }, + "node_modules/@capacitor/local-notifications": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-8.2.0.tgz", + "integrity": "sha512-fvLY0w2w4MiX+DD4+Wv4DOwOLdzKZsMDwAcRv/Juudd+QbKbn69s6cM3xVqPwAiDqfnqsY4/S8xtQD6M73wY2A==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/push-notifications": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.1.tgz", + "integrity": "sha512-WqzjPKIbYbARMN+GC0XMAJcxJpUUzqgzS/Ny8RODLrro38pQhm3GXYwX2Mwd+LZlLY39rGImkCkrKyQSNfuikA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, + "node_modules/@capgo/capacitor-audio-session": { + "version": "8.0.40", + "resolved": "https://registry.npmjs.org/@capgo/capacitor-audio-session/-/capacitor-audio-session-8.0.40.tgz", + "integrity": "sha512-A8suDS9TssaG9QFO9oLvyAysGa8v4G+7pL9QmJ327/jaEzCiUyTe9irAQBPPpjDZfZI17yYURMBkrnnLqUsOtw==", + "license": "MPL-2.0", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz", @@ -5696,6 +5970,318 @@ "@swc/helpers": "^0.5.0" } }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-fs/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@ionic/utils-fs/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ionic/utils-terminal/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-terminal/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -11948,6 +12534,133 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@stencil/core": { + "version": "4.43.5", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.5.tgz", + "integrity": "sha512-cgWD+GeuvJpTe1WQn40p02+BJ2j0j1YJ17GdkF2qKIQ23s2e3Zivq5yISXS3dcuV6oUJFN93jprdk+nk/sq99Q==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0" + } + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@stylistic/eslint-plugin-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-4.4.1.tgz", @@ -12846,6 +13559,13 @@ "@types/node": "*" } }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -14938,7 +15658,6 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -15254,6 +15973,16 @@ "node": ">=14.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -15385,6 +16114,19 @@ "license": "MIT", "optional": true }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -15407,6 +16149,12 @@ "node": ">=8" } }, + "node_modules/browser-fs-access": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", + "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", + "license": "Apache-2.0" + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -18819,6 +19567,26 @@ "dev": true, "license": "MIT" }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/elementtree/node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -22385,6 +23153,12 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -22925,6 +23699,19 @@ "node": ">=10" } }, + "node_modules/jeep-sqlite": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", + "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.20.0", + "browser-fs-access": "^0.35.0", + "jszip": "^3.10.1", + "localforage": "^1.10.0", + "sql.js": "^1.11.0" + } + }, "node_modules/jest-diff": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", @@ -23234,6 +24021,48 @@ ], "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/katex": { "version": "0.16.33", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.33.tgz", @@ -23295,6 +24124,16 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -23539,6 +24378,15 @@ } } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -23977,6 +24825,24 @@ "pathe": "^2.0.3" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/localforage/node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -25854,6 +26720,42 @@ "dev": true, "license": "MIT" }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/native-run/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -27053,6 +27955,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -28941,6 +29849,30 @@ "node": ">=10" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -30817,6 +31749,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -31092,6 +32030,13 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -31328,6 +32273,16 @@ "wbuf": "^1.7.3" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -32364,6 +33319,16 @@ "tslib": "^2" } }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -35878,6 +36843,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 4a6ad06..5f401da 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,13 @@ "test:e2e": "cd e2e && npx playwright test", "test:e2e:ui": "cd e2e && npx playwright test --ui", "test:e2e:debug": "cd e2e && npx playwright test --debug", - "test:e2e:report": "cd e2e && npx playwright show-report ../test-results/html-report" + "test:e2e:report": "cd e2e && npx playwright show-report ../test-results/html-report", + "cap:sync": "cd toju-app && npx cap sync", + "cap:open:android": "node tools/cap-open-android.js", + "cap:open:ios": "cd toju-app && npx cap open ios", + "cap:apk:android": "bash tools/build-android-apk.sh", + "cap:build:android": "npm run build:prod && npm run cap:sync && npm run cap:open:android", + "cap:build:ios": "npm run build:prod && npm run cap:sync && npm run cap:open:ios" }, "private": true, "packageManager": "npm@10.9.2", @@ -65,6 +71,15 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "@capacitor-community/sqlite": "^8.1.0", + "@capacitor/app": "^8.1.0", + "@capacitor/camera": "^8.2.0", + "@capacitor/core": "^8.4.0", + "@capacitor/device": "^8.0.2", + "@capacitor/filesystem": "^8.1.2", + "@capacitor/local-notifications": "^8.2.0", + "@capacitor/push-notifications": "^8.1.1", + "@capgo/capacitor-audio-session": "^8.0.40", "@codemirror/commands": "^6.10.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-json": "^6.0.2", @@ -112,6 +127,9 @@ "@angular/build": "^21.0.4", "@angular/cli": "^21.0.4", "@angular/compiler-cli": "^21.0.0", + "@capacitor/android": "^8.4.0", + "@capacitor/cli": "^8.4.0", + "@capacitor/ios": "^8.4.0", "@eslint/js": "^9.39.3", "@playwright/test": "^1.59.1", "@stylistic/eslint-plugin-js": "^4.4.1", diff --git a/server/README.md b/server/README.md index 2c038ce..b23f538 100644 --- a/server/README.md +++ b/server/README.md @@ -22,6 +22,7 @@ Node/TypeScript signaling server for MetoYou / Toju. This package owns the publi - `data/variables.json` is normalized on startup and stores `klipyApiKey`, `rawgApiKey`, `releaseManifestUrl`, `serverPort`, `serverProtocol`, `serverHost`, `serverTag`, and `linkPreview`. When `serverTag` is empty, `GET /api/health` falls back to the server's public URL. - `openApiDocs.enabled` in `data/variables.json`, or `OPENAPI_DOCS_ENABLED=true`, exposes the plugin support OpenAPI document at `/api/openapi.json` and a small docs page at `/api/docs`. It is disabled by default. Plugin support is metadata-only: the server stores install requirements and event definitions, but arbitrary plugin data persistence is disabled. - `RAWG_API_KEY` can override `rawgApiKey` for the `/api/games/match` now-playing metadata resolver. Successful matches include a preferred store link from RAWG store metadata, with Steam selected first when available. Negative game-match results are stored in the SQLite `game_match_misses` table so non-game process names do not repeatedly consume RAWG quota. +- Mobile push dispatch uses optional credentials from the repository-root `.env` file (see `.env.example`): `FCM_SERVICE_ACCOUNT_PATH` or `FCM_SERVICE_ACCOUNT_JSON` for Android, and `APNS_KEY_PATH` / `APNS_KEY_ID` / `APNS_TEAM_ID` (+ optional `APNS_BUNDLE_ID`, `APNS_USE_SANDBOX`) for iOS. Device tokens are stored in the SQLite `device_tokens` table via `POST /api/users/device-tokens`. - Packaged server builds store `metoyou.sqlite` in the OS app-data directory by default so upgrades do not overwrite runtime data. On first start, the server copies forward legacy packaged databases that still live beside the executable. - When HTTPS is enabled, certificates are read from the repository `.certs/` directory. diff --git a/server/src/db/database.ts b/server/src/db/database.ts index 526f210..b632409 100644 --- a/server/src/db/database.ts +++ b/server/src/db/database.ts @@ -20,7 +20,8 @@ import { ServerPluginEventDefinitionEntity, PluginDataEntity, ServerPluginSettingsEntity, - PluginUserMetadataEntity + PluginUserMetadataEntity, + DeviceTokenEntity } from '../entities'; import { serverMigrations } from '../migrations'; import { @@ -270,7 +271,8 @@ export async function initDatabase(): Promise { ServerPluginEventDefinitionEntity, PluginDataEntity, ServerPluginSettingsEntity, - PluginUserMetadataEntity + PluginUserMetadataEntity, + DeviceTokenEntity ], migrations: serverMigrations, synchronize: process.env.DB_SYNCHRONIZE === 'true', diff --git a/server/src/entities/DeviceTokenEntity.ts b/server/src/entities/DeviceTokenEntity.ts new file mode 100644 index 0000000..38862e1 --- /dev/null +++ b/server/src/entities/DeviceTokenEntity.ts @@ -0,0 +1,25 @@ +import { + Entity, + PrimaryColumn, + Column, + Index +} from 'typeorm'; + +@Entity('device_tokens') +@Index('idx_device_tokens_user_id', ['userId']) +export class DeviceTokenEntity { + @PrimaryColumn('text') + id!: string; + + @Column('text') + userId!: string; + + @Column('text') + platform!: 'ios' | 'android'; + + @Column('text') + token!: string; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index b857605..267ba76 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -17,3 +17,4 @@ export type { ServerPluginEventDirection, ServerPluginEventScope } from './Serve export { PluginDataEntity } from './PluginDataEntity'; export { ServerPluginSettingsEntity } from './ServerPluginSettingsEntity'; export { PluginUserMetadataEntity } from './PluginUserMetadataEntity'; +export { DeviceTokenEntity } from './DeviceTokenEntity'; diff --git a/server/src/migrations/1000000000010-DeviceTokens.ts b/server/src/migrations/1000000000010-DeviceTokens.ts new file mode 100644 index 0000000..7e502fa --- /dev/null +++ b/server/src/migrations/1000000000010-DeviceTokens.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DeviceTokens1000000000010 implements MigrationInterface { + name = 'DeviceTokens1000000000010'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "device_tokens" ( + "id" TEXT PRIMARY KEY NOT NULL, + "userId" TEXT NOT NULL, + "platform" TEXT NOT NULL, + "token" TEXT NOT NULL, + "updatedAt" INTEGER NOT NULL + ) + `); + + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_device_tokens_user_id" ON "device_tokens" ("userId")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "device_tokens"`); + } +} diff --git a/server/src/migrations/index.ts b/server/src/migrations/index.ts index 1545a3d..3aea51e 100644 --- a/server/src/migrations/index.ts +++ b/server/src/migrations/index.ts @@ -8,6 +8,7 @@ import { GameMatchMisses1000000000006 } from './1000000000006-GameMatchMisses'; import { PluginSupport1000000000007 } from './1000000000007-PluginSupport'; import { ServerPluginInstallMetadata1000000000008 } from './1000000000008-ServerPluginInstallMetadata'; import { ServerIcons1000000000009 } from './1000000000009-ServerIcons'; +import { DeviceTokens1000000000010 } from './1000000000010-DeviceTokens'; export const serverMigrations = [ InitialSchema1000000000000, @@ -19,5 +20,6 @@ export const serverMigrations = [ GameMatchMisses1000000000006, PluginSupport1000000000007, ServerPluginInstallMetadata1000000000008, - ServerIcons1000000000009 + ServerIcons1000000000009, + DeviceTokens1000000000010 ]; diff --git a/server/src/routes/device-tokens.ts b/server/src/routes/device-tokens.ts new file mode 100644 index 0000000..ce5e70a --- /dev/null +++ b/server/src/routes/device-tokens.ts @@ -0,0 +1,58 @@ +import { Router } from 'express'; +import { + dispatchPushToUser, + listDeviceTokensForUser, + upsertDeviceToken +} from '../services/push-dispatch.service'; + +export interface DeviceTokenRecord { + userId: string; + platform: 'ios' | 'android'; + token: string; + updatedAt: number; +} + +const router = Router(); + +router.post('/', async (req, res) => { + const { userId, platform, token } = req.body as Partial; + + if (!userId || !token || (platform !== 'ios' && platform !== 'android')) { + return res.status(400).json({ error: 'Missing or invalid userId/platform/token' }); + } + + await upsertDeviceToken({ userId, platform, token }); + + res.status(201).json({ ok: true }); +}); + +router.get('/:userId', async (req, res) => { + const records = await listDeviceTokensForUser(req.params.userId); + + res.json({ + tokens: records.map((record) => ({ + userId: record.userId, + platform: record.platform, + token: record.token, + updatedAt: record.updatedAt + })) + }); +}); + +router.post('/:userId/dispatch', async (req, res) => { + const { title, body, data } = req.body as { + title?: string; + body?: string; + data?: Record; + }; + + if (!title || !body) { + return res.status(400).json({ error: 'Missing title/body' }); + } + + await dispatchPushToUser(req.params.userId, { title, body, data }); + + res.json({ ok: true }); +}); + +export default router; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 8c6ba7c..0da0d34 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -5,6 +5,7 @@ import linkMetadataRouter from './link-metadata'; import gamesRouter from './games'; import proxyRouter from './proxy'; import usersRouter from './users'; +import deviceTokensRouter from './device-tokens'; import serversRouter from './servers'; import pluginSupportRouter from './plugin-support'; import openApiDocsRouter from './openapi-docs'; @@ -18,6 +19,7 @@ export function registerRoutes(app: Express): void { app.use('/api/games', gamesRouter); app.use('/api', proxyRouter); app.use('/api/users', usersRouter); + app.use('/api/users/device-tokens', deviceTokensRouter); app.use('/api', openApiDocsRouter); app.use('/api/servers', pluginSupportRouter); app.use('/api/servers', serversRouter); diff --git a/server/src/services/push-dispatch.service.ts b/server/src/services/push-dispatch.service.ts new file mode 100644 index 0000000..5c9d597 --- /dev/null +++ b/server/src/services/push-dispatch.service.ts @@ -0,0 +1,283 @@ +import fs from 'fs'; +import http2 from 'http2'; +import path from 'path'; +import { + createPrivateKey, + createSign, + sign +} from 'crypto'; +import { getDataSource } from '../db'; +import { DeviceTokenEntity } from '../entities/DeviceTokenEntity'; + +export interface PushNotificationPayload { + title: string; + body: string; + data?: Record; +} + +interface FcmServiceAccount { + project_id: string; + client_email: string; + private_key: string; +} + +let cachedFcmAccessToken: { token: string; expiresAt: number } | null = null; + +function readJsonFile(filePath: string): unknown { + const resolved = path.resolve(filePath); + const raw = fs.readFileSync(resolved, 'utf8'); + + return JSON.parse(raw); +} + +function resolveFcmServiceAccount(): FcmServiceAccount | null { + const inlineJson = process.env.FCM_SERVICE_ACCOUNT_JSON?.trim(); + + if (inlineJson) { + return JSON.parse(inlineJson) as FcmServiceAccount; + } + + const filePath = process.env.FCM_SERVICE_ACCOUNT_PATH?.trim(); + + if (filePath) { + return readJsonFile(filePath) as FcmServiceAccount; + } + + return null; +} + +function base64UrlEncode(value: string | Buffer): string { + return Buffer.from(value) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); +} + +async function getFcmAccessToken(serviceAccount: FcmServiceAccount): Promise { + if (cachedFcmAccessToken && cachedFcmAccessToken.expiresAt > Date.now() + 60_000) { + return cachedFcmAccessToken.token; + } + + const now = Math.floor(Date.now() / 1000); + const header = base64UrlEncode(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); + const claimSet = base64UrlEncode(JSON.stringify({ + iss: serviceAccount.client_email, + scope: 'https://www.googleapis.com/auth/firebase.messaging', + aud: 'https://oauth2.googleapis.com/token', + iat: now, + exp: now + 3600 + })); + const unsigned = `${header}.${claimSet}`; + const signer = createSign('RSA-SHA256'); + + signer.update(unsigned); + signer.end(); + + const signature = base64UrlEncode(signer.sign(serviceAccount.private_key)); + const assertion = `${unsigned}.${signature}`; + const response = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion + }) + }); + + if (!response.ok) { + throw new Error(`FCM OAuth token request failed with status ${response.status}`); + } + + const payload = await response.json() as { access_token: string; expires_in: number }; + + cachedFcmAccessToken = { + token: payload.access_token, + expiresAt: Date.now() + payload.expires_in * 1000 + }; + + return payload.access_token; +} + +async function sendFcmMessage(token: string, notification: PushNotificationPayload): Promise { + const serviceAccount = resolveFcmServiceAccount(); + + if (!serviceAccount) { + console.warn('[push] FCM credentials are not configured; skipping Android dispatch'); + return; + } + + const accessToken = await getFcmAccessToken(serviceAccount); + const response = await fetch( + `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + message: { + token, + notification: { + title: notification.title, + body: notification.body + }, + data: notification.data ?? {} + } + }) + } + ); + + if (!response.ok) { + const errorBody = await response.text(); + + throw new Error(`FCM dispatch failed (${response.status}): ${errorBody}`); + } +} + +function resolveApnsConfig(): { + key: string; + keyId: string; + teamId: string; + bundleId: string; +} | null { + const keyPath = process.env.APNS_KEY_PATH?.trim(); + const keyId = process.env.APNS_KEY_ID?.trim(); + const teamId = process.env.APNS_TEAM_ID?.trim(); + const bundleId = process.env.APNS_BUNDLE_ID?.trim() || 'com.metoyou.app'; + + if (!keyPath || !keyId || !teamId) { + return null; + } + + return { + key: fs.readFileSync(path.resolve(keyPath), 'utf8'), + keyId, + teamId, + bundleId + }; +} + +function buildApnsJwt(config: { key: string; keyId: string; teamId: string }): string { + const now = Math.floor(Date.now() / 1000); + const header = base64UrlEncode(JSON.stringify({ alg: 'ES256', kid: config.keyId })); + const claims = base64UrlEncode(JSON.stringify({ iss: config.teamId, iat: now })); + const unsigned = `${header}.${claims}`; + const privateKey = createPrivateKey(config.key); + const signature = sign('sha256', Buffer.from(unsigned), { + key: privateKey, + dsaEncoding: 'ieee-p1363' + }); + + return `${unsigned}.${base64UrlEncode(signature)}`; +} + +async function sendApnsMessage(deviceToken: string, notification: PushNotificationPayload): Promise { + const config = resolveApnsConfig(); + + if (!config) { + console.warn('[push] APNs credentials are not configured; skipping iOS dispatch'); + return; + } + + const host = process.env.APNS_USE_SANDBOX === 'true' + ? 'api.sandbox.push.apple.com' + : 'api.push.apple.com'; + const jwt = buildApnsJwt(config); + const payload = JSON.stringify({ + aps: { + alert: { + title: notification.title, + body: notification.body + }, + sound: 'default' + }, + ...notification.data + }); + + await new Promise((resolve, reject) => { + const client = http2.connect(`https://${host}`); + const request = client.request({ + ':method': 'POST', + ':path': `/3/device/${deviceToken}`, + authorization: `bearer ${jwt}`, + 'apns-topic': config.bundleId, + 'apns-push-type': 'alert', + 'content-type': 'application/json' + }); + + request.setEncoding('utf8'); + request.on('response', (headers) => { + const status = Number(headers[':status'] ?? 0); + + if (status >= 200 && status < 300) { + resolve(); + return; + } + + let body = ''; + + request.on('data', (chunk) => { + body += chunk; + }); + + request.on('end', () => { + reject(new Error(`APNs dispatch failed (${status}): ${body}`)); + }); + }); + + request.on('error', reject); + request.end(payload); + client.on('error', reject); + request.on('close', () => client.close()); + }); +} + +export async function upsertDeviceToken(input: { + userId: string; + platform: 'ios' | 'android'; + token: string; +}): Promise { + const repository = getDataSource().getRepository(DeviceTokenEntity); + const id = `${input.userId}:${input.platform}:${input.token}`; + const record = repository.create({ + id, + userId: input.userId, + platform: input.platform, + token: input.token, + updatedAt: Date.now() + }); + + await repository.save(record); +} + +export async function listDeviceTokensForUser(userId: string): Promise { + return getDataSource() + .getRepository(DeviceTokenEntity) + .find({ where: { userId } }); +} + +export async function dispatchPushToUser( + userId: string, + notification: PushNotificationPayload +): Promise { + const tokens = await listDeviceTokensForUser(userId); + + await Promise.all(tokens.map(async (record) => { + try { + if (record.platform === 'android') { + await sendFcmMessage(record.token, notification); + return; + } + + await sendApnsMessage(record.token, notification); + } catch (error) { + console.error('[push] Failed to dispatch to token', { + userId, + platform: record.platform, + error + }); + } + })); +} diff --git a/toju-app/CONTEXT.md b/toju-app/CONTEXT.md index 066fea4..ae9a0ec 100644 --- a/toju-app/CONTEXT.md +++ b/toju-app/CONTEXT.md @@ -17,7 +17,9 @@ Owns the user-facing Angular 21 desktop chat experience: rendering and orchestra |------|------------|------------------| | **Domain** | A bounded context under `src/app/domains//` that owns its own models, services, NgRx slice, and components — e.g. *chat*, *voice-session*, *plugins*. | "module", "feature" (Angular reserves these for different things) | | **Shared kernel** | Cross-domain contracts in `src/app/shared-kernel/` — wire-format models, P2P transfer utilities, plugin contracts, signaling contracts — imported by multiple domains. | "common", "core" | -| **Infrastructure** | Technical-runtime concerns shared by domains: `persistence/` (client-side store wiring) and `realtime/` (WebSocket adapter). Not a domain. | "shared", "lib" | +| **Infrastructure** | Technical-runtime concerns shared by domains: `persistence/` (client-side store wiring), `realtime/` (WebSocket adapter), and `mobile/` (Capacitor/native facades). Not a domain. | "shared", "lib" | +| **Mobile facade** | An injectable service under `infrastructure/mobile/` that wraps Capacitor or web adapters so domains never import `@capacitor/*` directly. | "Capacitor service", "native plugin" | +| **Capacitor SQLite backend** | Native persistence path selected by `DatabaseService` when `PlatformService.isCapacitor` is true; uses `@capacitor-community/sqlite` instead of IndexedDB. | "mobile database", "Capacitor DB" | | **Rules file** | A pure-function module suffixed `*.rules.ts` that encodes domain logic without Angular or NgRx dependencies — easy to unit-test. | "helpers", "utils" | | **Custom emoji** | User-created image emoji assets stored locally, synced peer-to-peer, and referenced from messages/reactions by stable `:emoji[id](name)` tokens. | "sticker", "emote" | diff --git a/toju-app/android/.gitignore b/toju-app/android/.gitignore new file mode 100644 index 0000000..af7c5c8 --- /dev/null +++ b/toju-app/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/toju-app/android/app/.gitignore b/toju-app/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/toju-app/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/toju-app/android/app/build.gradle b/toju-app/android/app/build.gradle new file mode 100644 index 0000000..4b98197 --- /dev/null +++ b/toju-app/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.metoyou.app" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.metoyou.app" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/toju-app/android/app/capacitor.build.gradle b/toju-app/android/app/capacitor.build.gradle new file mode 100644 index 0000000..16cd606 --- /dev/null +++ b/toju-app/android/app/capacitor.build.gradle @@ -0,0 +1,25 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-community-sqlite') + implementation project(':capacitor-app') + implementation project(':capacitor-camera') + implementation project(':capacitor-device') + implementation project(':capacitor-filesystem') + implementation project(':capacitor-local-notifications') + implementation project(':capacitor-push-notifications') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/toju-app/android/app/google-services.json.example b/toju-app/android/app/google-services.json.example new file mode 100644 index 0000000..2e05a66 --- /dev/null +++ b/toju-app/android/app/google-services.json.example @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "000000000000", + "project_id": "your-firebase-project-id", + "storage_bucket": "your-firebase-project-id.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "com.metoyou.app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "YOUR_FIREBASE_ANDROID_API_KEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/toju-app/android/app/proguard-rules.pro b/toju-app/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/toju-app/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/toju-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/toju-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/toju-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/toju-app/android/app/src/debug/res/xml/network_security_config.xml b/toju-app/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000..af5dfb3 --- /dev/null +++ b/toju-app/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/toju-app/android/app/src/main/AndroidManifest.xml b/toju-app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..df3c780 --- /dev/null +++ b/toju-app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toju-app/android/app/src/main/java/com/metoyou/app/MainActivity.java b/toju-app/android/app/src/main/java/com/metoyou/app/MainActivity.java new file mode 100644 index 0000000..1058344 --- /dev/null +++ b/toju-app/android/app/src/main/java/com/metoyou/app/MainActivity.java @@ -0,0 +1,13 @@ +package com.metoyou.app; + +import android.os.Bundle; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(MetoyouMobilePlugin.class); + super.onCreate(savedInstanceState); + } +} diff --git a/toju-app/android/app/src/main/java/com/metoyou/app/MetoyouMobilePlugin.java b/toju-app/android/app/src/main/java/com/metoyou/app/MetoyouMobilePlugin.java new file mode 100644 index 0000000..fb1fbdd --- /dev/null +++ b/toju-app/android/app/src/main/java/com/metoyou/app/MetoyouMobilePlugin.java @@ -0,0 +1,115 @@ +package com.metoyou.app; + +import android.app.Activity; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Build; +import android.util.Rational; + +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "MetoyouMobile") +public class MetoyouMobilePlugin extends Plugin { + @PluginMethod + public void setSpeakerphoneEnabled(PluginCall call) { + Boolean enabled = call.getBoolean("enabled", false); + AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); + + if (audioManager != null) { + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + audioManager.setSpeakerphoneOn(enabled); + } + + call.resolve(); + } + + @PluginMethod + public void startVoiceForegroundService(PluginCall call) { + Context context = getContext(); + Intent intent = new Intent(context, VoiceCallForegroundService.class); + intent.setAction(VoiceCallForegroundService.ACTION_START); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + + call.resolve(); + } + + @PluginMethod + public void stopVoiceForegroundService(PluginCall call) { + Context context = getContext(); + Intent intent = new Intent(context, VoiceCallForegroundService.class); + intent.setAction(VoiceCallForegroundService.ACTION_STOP); + context.startService(intent); + call.resolve(); + } + + @PluginMethod + public void enterNativePictureInPicture(PluginCall call) { + Activity activity = getActivity(); + JSObject result = new JSObject(); + + if (activity == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + result.put("supported", false); + call.resolve(result); + return; + } + + PictureInPictureParams params = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(16, 9)) + .build(); + + boolean entered = activity.enterPictureInPictureMode(params); + result.put("supported", entered); + call.resolve(result); + } + + @PluginMethod + public void startCallKitSession(PluginCall call) { + JSObject result = new JSObject(); + result.put("supported", false); + call.resolve(result); + } + + @PluginMethod + public void endCallKitSession(PluginCall call) { + call.resolve(); + } + + @PluginMethod + public void exitNativePictureInPicture(PluginCall call) { + Activity activity = getActivity(); + + if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode()) { + activity.moveTaskToBack(false); + } + + call.resolve(); + } + + @PluginMethod + public void isRemotePushConfigured(PluginCall call) { + JSObject result = new JSObject(); + boolean configured = false; + + try { + Class firebaseAppClass = Class.forName("com.google.firebase.FirebaseApp"); + Object app = firebaseAppClass.getMethod("getInstance").invoke(null); + configured = app != null; + } catch (Exception ignored) { + configured = false; + } + + result.put("configured", configured); + call.resolve(result); + } +} diff --git a/toju-app/android/app/src/main/java/com/metoyou/app/VoiceCallForegroundService.java b/toju-app/android/app/src/main/java/com/metoyou/app/VoiceCallForegroundService.java new file mode 100644 index 0000000..6daefd0 --- /dev/null +++ b/toju-app/android/app/src/main/java/com/metoyou/app/VoiceCallForegroundService.java @@ -0,0 +1,76 @@ +package com.metoyou.app; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; + +import androidx.core.app.NotificationCompat; + +public class VoiceCallForegroundService extends Service { + public static final String ACTION_START = "com.metoyou.app.action.START_VOICE_FOREGROUND"; + public static final String ACTION_STOP = "com.metoyou.app.action.STOP_VOICE_FOREGROUND"; + private static final String CHANNEL_ID = "toju-voice-call"; + private static final int NOTIFICATION_ID = 4101; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null || ACTION_STOP.equals(intent.getAction())) { + stopForeground(true); + stopSelf(); + return START_NOT_STICKY; + } + + createNotificationChannel(); + Notification notification = buildNotification(); + startForeground(NOTIFICATION_ID, notification); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Voice calls", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("Keeps active voice calls alive while the app is backgrounded."); + + NotificationManager manager = getSystemService(NotificationManager.class); + + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + + private Notification buildNotification() { + Intent launchIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, + 0, + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("MetoYou call in progress") + .setContentText("Voice call is active") + .setSmallIcon(android.R.drawable.stat_sys_phone_call) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_CALL) + .build(); + } +} diff --git a/toju-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/toju-app/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e31573b4fc93e60d171f4046c0220e1463075d9e GIT binary patch literal 7705 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 wKmFJ0_JHhk1&0B>;%YXATM literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/toju-app/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/toju-app/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..807725501bdd92e94e51e7b2b0006f69e0083a0b GIT binary patch literal 9251 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/toju-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..14c6c8fe39fcd51a0414866ad28cbe8ff3acb060 GIT binary patch literal 13984 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®q@N z14_H8ofW)l{|J8q+a)eH)I0r)>WXdzV%7J>PA~6_J)KLT90iYa^K=Wz7D!OybzqSru=f4?|KFl;Y)gP_H6V4x`~kZ6fE(xM1&;?72-TZNk+0 zr+Crr5yl%Iy@vfmt3eYFl!jIvPGFz^8Ek+2`48O1_pCX3xNWh-zBa{rIcc%+=|XVj zANYTg&s}TKb#OztQrCW(Xk?V^i{`q~%HtcveTxq(_HKeC9GzrtguMT4Nvs@KakPTA z9>*8bBZmLz`lK5=l)=b|=dT3a5ag^a1^znZyx5QKfUb1b9yacArRp%3@QWo(hrsCU z-K!-=jDmv!zb7XT>)r|-Z0Ry}lk2;dk-ECqMwr_nKN#x*X6~B5hVIN>6$1HwBz3Of z=Pk){AL5*=d90f17_qZEJLm;Q%WMdX=*N&!ki@E&cy7?>{1ssAH(tACtp*r@d^til z)x(1#6(kPD+joSF&J3sxJU@{-sWCS+pZq{Gsx=?z4wP;>?)1yHv0?X?VP{}cX4~aH zxeBPKw_rgW8rvewS1W2#^y+c>-183iMbJCqc38RN_o~__9-n|jcd&oA`m7*&Fqqpc z;Tev*0LS-ZK47Sq1unfvP1S43uA12P?PJmI8BeTYPr~R*tYUm^0;U%Hmu?bSZHEK6 zPjsW=E67Kq-&trmf;)UkmRABH2U)V)-eRT$j(%G12lLMsThSsU10iP#{)ZnvjzN$d z*K%P3`}oqyvpWP~venr>3viH8^`)Ma*=B31hw*Q+tqE>i2y7w!(o^lI^Yss^=tHW( z;cnCT(%B1gLz+TRGW9roFjI1EQTu-u`(f#RmZ8;FSN(bsC1J;+(i_R6mrW=yYx$cy z#%QKVrEx~kVMg~yo?^N28Wnk6x%L;J8i|*|ANEiNjq(Vhzuzl3ikpA*G!Z}kLAzAI z9qnySo%D|AuJj12%h;Otqjs(>LPj?rNdeU8so>P(C>XMzlho94ZD#w=cCOOU;=3&^ zsqAG!i{~lY271D|m>ztPV`)X@FO_;`wPjppYNQpM+ncvtz1lZjN>!Q^*I}T%uP78Z7tbV2$q3W_)14=kLFyJ z1GqL6T>ClgeZorL!}xP4f%OB_EsmJ`uw7dGWNV9OLlhb|UMpVhc{4@Bhh`tO!ZqzD zhusd<=K^ah!L@gQ?6dOpI-ge^e>S5W9eII57Zu16eU?GRbgKTeVk9yS{iK|O(zLR> zheb?;jwGCHS80NCn=jKxgJ>}qu4l%5NPihjzazGv#J?Jcyl;<#IW&x4mm>nrW8>}C z3U@aeD~)*F(0o^2{GnKVm$Jr#aZE ztl~TOkM^SdzJapQ((!-i8b!RkVQBKkL`2ZCBuy!qI1L{3Er526plVols~68U-^9Px zR(3{j;Z9RHX^muc0dUywJ|`yyZFf=k&-Gb#m4u73Lm5Ks%BfHj%2|gjn#i> zLC5pO$2Em9H;qoKQmMtl<@wgtPF1%2HariD5O~u>8=^*J&au~JH%Ih@&2Uging3U_ z0bzfKucW$ZHSx}!#buB?+-J)%RQbbXM-!BJTS&#dU_@lxU6>te2O+9 z@F{F{Nb!;{Cd`Gx+$G?11aB~S#wIH%D=*=7f7H@D@%B1)&bF$@t3JDq4l*%(wJTlh zo`?uMq{YilKUewPNaC)GuOr<8j9&ofqRU__BRUX^x8Cj3a;a$rXzgXqW>LR#CUn%~m)t zYC&ol(gAkbc^fd`xWU&bk5vT6KbFmsR=O78Bn%t7 znbw&=c+|T&#r+bls5rU6D#HMvqA<|;)BV%jOMonkm^p$7Vcel-Wwn$=uAJv&(8W>% z9))Fxpl*(%E#wFm_m!U~2HqgZs^2vaGeY(UfYKrSHV}w^D0N6!se5Ewy)Yy-!(2

aKj2hWG7>znxs|SE zN4rHtiSPqLskWp(?(_YYwgq+1@8v+~8As|(bC>$D(atG3ZE8-ZM3SVcg|vHQz$I=!(A`k`5= zOqR>&%G)$)k*QLz7MTB9wleWpv&N9Sta64wy}3Ytd?x!Ja8z>(z~(3UNFu^eFmn#6 zw!!gUxOuZi$PQIs*ixfZR3iLyADJ z5&s%tPfk>V!x|A-;oq%1!yk9H$UBP0ToA*EDtz(^!_AnF1bBQ7joj|? z5b)gSI8c8O$PYFE!vXJ<4gebg*9G9P2wcB{#kv0FItc5T@PDNo)}Rh4Us}L{e}xzW zhwt`)j`M)mP=G6H0;^&q=I0{jU%bIRkF#uLF;{vVC&H|_uc literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/toju-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..244ca2506dbe0fd8f6a05520ac7d1a629ea81438 GIT binary patch literal 17683 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/toju-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..bfabe6871a17a5e95b78fb30d49b7d2b4d2fe4c0 GIT binary patch literal 13346 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

VwO4Hu8X}%XHUAz_J@50rzbkIsat>4oWtQt< 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/toju-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..6929071268eb03ee0f088142b6523566b78550e2 GIT binary patch literal 17489 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 KR&><$`~L^lv%S~= literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/toju-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/toju-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/toju-app/android/app/src/main/res/drawable/ic_launcher_background.xml b/toju-app/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/toju-app/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toju-app/android/app/src/main/res/drawable/splash.png b/toju-app/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/layout/activity_main.xml b/toju-app/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/toju-app/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/toju-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/toju-app/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/toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c023e50595074292c7361183a64de08cf9686c9c GIT binary patch literal 2786 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/toju-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2127973b2d318df7085734d236d0ec649a2b0292 GIT binary patch literal 3450 zcmb7{i8s{W|Hmua$kI%{%-b@IR0=VZOlq2BVkpa4OGS){8Cf#2eUeX&8H~JzHd~*O zC3~Zhgsg+9>>43zd_~y_*^A$N&hIbyp7Xl*o^#K+=ib+SyKx`Gt}@5_%MQ`k+3nf>ds5S>KOkCHv)i zk%JRnO6Tlhh5-Jsl`@O=xwa>)9yo6*<6Kw7f2B#vqt{ffXw59+z8yvFZQkBQi9Al=F@*iA|!QS3Y2jYMcokoAzkn1?; zlfJzAcb^}FmdE0raY5uc5+TkMfgi*dRp{ZTi<7Xg`+(~F;^9}MP|bHSpO7I}Y4;wU z4gO@pDAcNMaG8~kB>CYdRLI$O)}>7a4$M78&pP6`GFiHy8^n!dee4Om4RFr12-Ma6 z_u_hW^)c4>CFEAT6hsiCtOev8(d?YO7p<_y}I- z=VME#+1(_#N(yAYVyRM{Y!K@$54zz*o-CYND2xB0&o;-dpBaeZzFB2qfI>5J*=c{Q zwP1epORF=o)kJ4nilo55O1xl=av)mPQ#N4d9YJ^V!nN58dOz5!Npg9G;eX?l!VYdh z`$#i?N>02>J*1^~3!l-oH04=iwD;S@CjR$-v!SJa&xI(0p{8w}cJrGpz2>-j*!g;0 zj2CG7=!O%j&mX=-Pll>Lgxsmr(d5jLtsVA2hPz-&DZBYowFfL9WK>8q2K0|mnnh!V zmu4-Q?@XZIEN)n_Zls`Er#}&+4Z*W{Q_a=Q7OQ9+);cAV8~2~ z4*!LaUie1^ETg#6?xKs3PA%c^tenXEjW0?bp{HhqKbkEenZNB=8t$!{r>pO}#3sL@ zv_o6f*M>?z6iaw2=ERQxR<~t91~uC)d!)eR6RO7)BOIJwIJ~J<$bq*zLscK z0r&KJIHx8CqtE*X8Oz#Ow&13%rYvjUzE~{nB^T$h@ zFC~8s;e7$#PDoGBDcf9>tad0#^|J_iq8DN2560lg^q<($@f3M}zOZc?oPfFAc6xSH zZL`6}tzt<1JeU$~-&Md!jv0NmNK?N>*2vZ&6d4dIEyiL8FlbsF*JswaX)P-dV@-j4 z-`@UuJcxA?D69i){yYwdq8A*+hSIsdVofP~M`==k^hlLa+|Q1d=XCU0 z%r)Vab?26WK0^l|ZHKGAKbkCO&1Sz|VPPy1Wc5(@SG);Vs{CRnv3q+2dq-Bc)7@== z>05l+5$pN&AP>DaHw`ogk>!oy5k%HFMuCe+t383ijS|0inFMjg?O|GvLxW*K9wikw z=8-|jV~v%%u&r;^P?LwNl>E}XMPZi--$H+i)DE?s9egiNc6+-bzYmT_fD*iS-@Dh= zRQB*k@1q@j-j!>YHxBf&MFpECv^%c(`+E$Oc>9sN7f9hqFMN7GgMMR!=7f^RM8xC1 zKMh zug|?-KwuGYy+c_d0jaWjS;cY}DGOmp3r+Xg2Tf9!l- z(>Y;SZXYF|dhsi;izRubirTyWr#?Ci9J=3^=a!%r>=`}frvf#CDP>js5hK<7sHfBn zqrD;UChm0~DX1J&$l!7)H9>de1*IqXE`$Qd!AXSK+@7=$e-C32a9ajLAkOF&`RtB` zhHA*3SyXLnB3CYJH(zL4jw`+l&vNLh6wZ?_OOW9Ft3s{I8czelk9{fg7GQKy6}TLE z9tN!arzF+09G4lGwhGo!1P37*sFhoNCGoV*V9UG}RBgrY2*Ov=pd<;w7pB~BLU*PS zuj7JW`N)ZgRtzu-v_eTwu_yJz({N;MLK7p?81@7$>DV2>GY-4$yl%{r( zl5};Q!J9;>e1q5JH`AnYteB#3DFSYHqMNfaZA#}vZwhMQwf9Ee;lb=jd4Cga^KA;p zG4lPKe}?@!%Io43p^BQ`O|)Y$S6x(*atprZXP73t=81c3)`X2zyWkCCjhg=qsRZ$l z7aoMT|Bi)fpYAm92Yt8D>YGRts_-IKLX7L<`K>xxhDmfw>3^pL;Dm-BcP?SI>SzBW z-pu*TEhZdf)^FuQwMz|J1l%Y+JVbeOX12D%CV--LEug{_&fvRsc6o*$;}gBOXGI1>`Tn z?N^kt_3<1+Cv;2KBGt6Fp%VNkPs$bh5k~lXsBpu-pq7~$Ih5CNLBC0KAOkBVCE&g9 zD<&;RbyOw@uh6o!YWT5siF&H-e(%yJ+Yt8;Ls-`O#X8%8IX5TO6KB_|pp+YWNPUjL z2w4pHT)^Ge)kUetOfKG&j@%lM;^)mr&mo#kn=2n%ag%*Qt#KotiAoohN4>Fxbmzoz zxi)(Lmm^YrM~15S11sOh{w*q_ph#Uu;>x`l>8{8J?ymvWTYMQKNTlB*>J@BgG*?a} zB0Tk=?BT)K0T%b8;nlSgMPXEGX(BLpKm~KyjC*b%_ z%9=T8HEri4jlG-FWcdF&ZSNh`T!MoI6t=5R947^N^9DbBdJ7O}l zx#Gg28lz8-U4@g;YB?6cw-PJe%j{b$Ar@%CQg=hD=9i&uw~bpK{Xp?5v(h4%_0rX3 zhG+Tex<2zHFnM?VC|(1|=$1I)`$M=j?5v^Mk%8XJqsYz{S(V&#`1hFu0*+ zw@AoU&m!6{zv^^9w947bvv`guGTe~;|D4#!ta#OEoW}pQ(tma~RNiwnVJ@&S8fDVt zwY}qJriL>@@4R7{Ql{-MI+yCsqdHJzJx#I?6Rt2Nc5#NpoSC$eu)yjg{PM*O>v*B* zOm|;hGRFMS)bMQ$pbfHT%f7N{;8(RDTGHNPM(@aeZ)y=PYf@t;9RF$D>mGM{dB8(9 z@0q`&pZ}cn+hISr2$9uO%8o7lrthlEBmu_dOI4Kh4?)Ik?a%`<7a?y0RD;oZ>0QI( zI03s`f`DMUHXZA@XTyG@&qwyBhrBuD4C|Cj9C_17jp`0f%^N=#!u!x$ z(pkVje^Kx8i1K7~ONzoL7>ZjAd@3g}d;>JqS@fQ1q<4#JN#Vb$*UquKjR@`OSi9VI zNC--#qatzs3JNKJ$P4OiIK-KKacl<(PI&y8tH`fZ*1B-vvQRt)GQ`fbV%prfcJhD< z9N_l3GSJ(&Rme0u-+=j@jm8}Eg5@37BFxkkUYdLRTJ?m9dATLj?|U{oN$8ZB*oNK}xC{!P)0y>vu^Y<=Px>M;* z5noIVUShb0{2;1E^E9Tz$6>pfZpFoO5m|$Uy7_kuGr3>K%g$=Vd$NKN^zTfx9-RP~a5$nENHDh&;g)3l3|1A=;RvdV z(Yp9|j<9Oer54~M{=OfT&n2>!h^%N050NhroE9%o?A=WgqA)6_PMXzh4>z zw=%=QT@n`J<^oQTyufjalgySwA%@xA6g@7J!i9x}KR++-W{7c8Xk;pa=0w4fqTNbs zI2Y)6AUU~}dz=&-8UT)Btw|cBy86cAX5HG)WWg+S=M})U^%?0}|#JiA3gsx)?U$255v6gosCX3rny#DIqv!NLFqY z3r7-zg-ou-N=iyzOvu$lvKI=4`VyR=h%KUmKo`M{a7(gtH%h5kM!w8W*R(U3q>^8! zunnhW7Le@E0X)DIeZgSk_xpoKj~@N8vCnYZzb@PsmKGssyNXpd zV~5HX|3_to4T%FPqH7oJQrX1KDqDX_uRF$C+bkz4MnXR-D=Rl}SL+z(s1>>&iKobK zEQ7$1`_OIH2?{IOmw76CIzwgCwySLFHkHk%4(8P*VmT_Clc};tH>zy>29-_Nq_X#q zMMHABk(h7@27~Oxi4&tdyP6O!8YMC?Y9aCDQx?`kbJ$&A#mEvSI9m!-Hk*L_o~-A| z?QM`=$yQlzZ5(jwRrUha^VlXGkP#9r3GNnhv2rmYS5#E&@+8>8%!ukx5fCCP*MmUC zQT)8PTUN2mX6_`{BI}u+5ew<1J>J}{fZ(L=R@vj5bU=@7jD`ev+i*&DwQhI^`blXB zJ96a6{jORhOe`D@zYp!fM3)ExJq_J9kZ{MLMdeL0TlTfep31a_G>srF%u?BzT6@a@ zoOuNK34-wWW@|{$r2JaNQ^d`uDp_%H@u!Y%Cd`Y>tp{Crj%$vpc#LoC+|B%XQKQ-l zX}XOi!QHGN0nW>0WfQ;D0mVStq-#=9y$+L0gc~GHjOyy@YS*q^dy8%GD9AN6H9gHM zR@DT8K*Tk|k`&b%T1k`2{zQ;wWZJtMEXn9@fCCqt_>~T5f(4`(EW}ksIx-}$d z5=J$}GA(hoB+*yO(Y6wU*hzn}OtXLg{_7|duTTtuVIq()T4noWO>>+;!b05K4VLs0 z#Y&U6sO)zrfb^9PX#$4?O)98Y9j8HD+)Z<{ii*mr)vH(cG%|@Ay?j33NHMSMWv(S; z69i_KP;9eTWv}Ou_%%7t0+#tzw)`7=O9G^+TU9n?s|gafB)A>mcuPy{-3?1hb#;ww z*REY-!D9FB-N`ZfLqfHPN6w1Z38|=Tp5W{tIE%=2=8(|Np*z1KM`b?0{oPeKB&X3$ zgS0Ig65K5{%iVJ9-Ays4MM{_?;n=ZbOLvW5N`-~dy-sO5>`vhvyMeY zMF)sQ=T?t70cUp;A;42EApl0V zcCWbwC9UX-lZqzP<>lr10|yT5)+*E@DEj1+Pi~--s#KQ|%ql__Lpii&d5$xrKOfN{ zIcDfQm~@JhQuic23t*gzF}RxssG5${)YP_sQ2^SuZChW`lSZ$A%y3#eM;7pxZ>=Fc zZ?GgU?$(~P#BnmuG*88aTgA#X4OC5Uz4g|>7(iRLY#GoT3Ao#T7qcXZa&Fv`qvmu*VNSb7cXA+Gb12v6iZ7>>lukes<;cO zuq4?4$?N2p6(l!HTH!HHdd2u8RW1A^nIV>Al84QJb9Su|lMs zTUqiNyjjxuNJ|z|eWz;N7cs3C70;YG^9`{`iQ1#h!|JQ5s;bsmt-B-Qlx3w|fXYZm zL?G!V-0e+Euv(IlAJLYm;@AnIr5ZUoIsXESs3{Mgn02N+WQj;t&1>bt-4cOQLU(-y z!Fhr1{DK4akj_V1g4Gf%DPi5s%Z3RYzJ06?C{}aCT3Ec6 z1pWEzcPe}F8yb$kc83&iTC&VAx?!~hOG?INA8)%#6vz*2Y;0ujyz|aPurN<|z}J$d zOqtS$D3*)eq_!&<9wEW4-ae_aMF%+`Go%CUPfH3L6oRB^t0h=c!n#|$TW^Fwmz0!L zju|s%0MF*5A9>)!t}ZPt`wEt0m(lY$$rTddyh)amdPoPPK{^->>5Xsgg*%?Kq`XmI zQVPq7ZoSd<=itGE8N}e4DC;rlP}hC?_RVw4=mjJ@ck>aPHK$be@?i?#4(pHvC|D8- zGzkzfx~)8xcv_+l<&riB9?z~4(=L9s$?s=t*Z%WmFgSepa3{+rapx{suTXZGg;>ph=~H_NOK0^g-gV;(??Y0_kpEVbQsVAAT4ct2)^}QM7*j z`p)!n-PyBeJ?a}3pB|WXn$H_mp*t&D~ymZljsiw z8M)Qx=sRcNxb)nWvf1BI+QGa`;0s7Tzry~WtHaR%nING+lga|^OiQS~3cquN>~(1> z6vk$EnVma#jxAZT?B)e4hv_Hvd!4Ue{&=gbnuV6 zS_MV8$D$#jK$Cm{@3B*UgSES1wFFB_VVQ4;iX^s)OV;*xhg;CM@`@_9bm`J3(dYNx zd(Yd>*BLWrTuCoCpFDYTGoP=Oz1$_48j@Zb4QbWM_~004CXLg#SS`VllB5`BG%W@R zE9=G$GzNtPN9z-0Br7WmtEo5hK6^VzsGvpNCQqI`98-|oiqsfC55@X9AipF+US@lI5lcAn%u`_lSd%{_9>!A|8XDM#AAYz3 zeO$0$!BvTDbnS58efMGcqyO>a$9KRSwcVj!cChlTd0t$=%boWU1UhZv(%eehnM-wr zWzDtr?Af!E`gR-dV`5KIbF;g)SFc`o6&4oe^JgDq=Z3c3O|Lp(52sCFB`L8@T*jql z=nnpU^ys$*J$v>Xg1$ZX+;i=FB!MdEN-sA~pFVwTQIW3+zH2q~+fC-Tr6qF0aGahd ziuo?IL6)OtAUGT?WiKcw@Kd(%Tl9(dt^LQ;sZ&$v<(9Oxw5MxoYE&yoZcp@hwWL;k zQyfMm5AKHe#tg^j^QjaN&Z55b=6yPEKT^6Qf?y1@(3hp}VFUVA>_h$CtE@ZqSqKWWpmrKP2f`p$b<_BypG zG|9@{?A$8e{YiS9Bk>?n)-;FQs%i1!#ju?I!-fsRg!~12&^PJ92Oq>QAM~3xZQ8hF z-<>E3G;1M%8qbCY^N15K96LxnLe}COv zNl8h$J3Bi&qrSeL8CQ8Ct0np!Z(lG;fLa>;Az9TQn8RkwhIik6cO|tA5A*io zZN2Ef8q;COkRe_B^y!05j{`=I962g8Gc&!qx>~ag4ob0eJrM+Y*`@C^myG!wOj#gO z_LWs&RbtDC5hL&b?*Oz7ZM+n4j7Rd&p+n)@w^F%-!uF^3?%lfwBOPV~_#u9S1OIC= zYL3y}JOF9obtqb$WHC+tW<0T@;ydThU+@gtfVS{9T{b^7 zRBtNSv2`ci-Cr$SxbWGJKKkg*jEsyga&mGGoF+3MQ7tbkE32)ntZFPSE^90)DXAsV zFVD@*J%e*d+rIet(r^!FR0v&PTza)y^lQbzyqUE@E)Jh-+qa6a4x?F*WQAjF j!Fzm$zi}9sOmP1PRa@72Hy+?#00000NkvXXu0mjfxSd z#Lfy~3D{sKwzH9i;2=l{N}m08$9`|7XWGqI)35z{dV1z9Msf}rz0&LH>8Y--x~jUW zXWqPP*HwH8AzDE5=a^cW5&U|ht4NXc%cBoOdlBeP&>eF`H1{H#Y>C3-|7Osp>FMbd zV}6!%9wO#N`-pts&wAQ3x+k)YrE$Jrnx!HjaQhu_~)3AJ1*n6 zpCP@^^U!v}&vl|_5IAVNcn9FE<8(ey62^Me=aMoZSGupS?>1dl6Tp1>KXc~HJrFEU zS|&zGTBkk-8nS6VvJ!Zg#==w*$ElTY0?kVq2tctoQRwOExnGyn3ZTEg|6ZWZ(S)Ss zB-5b@$_|SFivvJoy_x`cFb+x-zMKQy(;^QW+O}=m;(7t(i5Z!QIiUif+bs351Q62Z zeQDEZ$APHYyf66&V?9pq(h78Sbv@7!fWT%g6OuqvB{u@h2EyjBHlCzyr=l30=VZwV ztN=dH8~}4drTI9Y&_N&s$F5AkQxjI%d}uW)W=t#45CEGKSD1wyHYvoi3MKkQMG;OV zRO-UZ)u<{l4<~471xzO$VDPDL7!grnI;?W&ktit$0IV!vkplKf4bED zHsgtvKXdW?w_LP+0}$VFF=PQzW>WapUI5rBL9F`;W-`S>!p|B2g)q{*O<=Q+>^*hG z7oEdgyl|ij;^hhmC0Gf*kLaNMCVW##H&AW@$m@S`?+{(4;N4FmaDM&xLlRE5UT(b3dQ;XAmIw_192eE z6}Wf@f_?%aQd8k^DS-IE0I?jxKf8~MXZFff*m44^!g%675hQjnKT0K<-pFhq&KG_0 zE==XMU$|J&ZwE1s`}YrV@uC6ZInb8hq)F9I09oT3Sv>$wW_+>cFhb_9VGv^mh)ju# z_Y4p(q1|_pM;vRLWPeFnr4FKFC=6nQ z1qAto>DaW{={q(v62e(+WK;&yO1Lb!k|G;D1`zA(0FknUSV}u+C2TAF|D+O*td5c# zj$--Nvs}C}5H%;$CoWqyyH!X*a+AzfVm1%Jt%NXJ#%U&IyDxCD8k&7`FbZO$B|7Pw z-3lagr_xdLnH2ygO{ZGa`*si{{GNZ1j6tJs!3jjsiOHOO<;fp+|j#LSb9Z40pqgf3)Kc{=N&-e&W}d@$vS^~fSS0ASq_ zxe7baOX1BBYi@R%PD;(!s_fKGJnN!9V)uwlSCGeic2)u)>b(#Z-Ugxr|EIIHD?3%x zy9|MZHB;34eguG7@=YRiZXSgbUzED(~Elh z(MPewM9HaLz>^lKWMnleJ9RRrYVfA&foM@*Ju$PbMqG%sr3WmbvQuUg@YIsIS?$EG z+KJ6WC*$-WZnLwU*x5jk*~ob!=F}8`T!jI5WKp4=%teKd1CexQ%0j1olBYW8GRDmm zG<6;B4h#(3CyRP%LQ3=Upv#XHK5{Zw&z0HOeWK#T*vPuOyB|yf5PUS)zJ2@c zs0T7Gqa4T!K5(dy4-}K>qeI+>jF}G_03K9QHO5tWvdH_DQ$f0SA)Cl%bJ@<$&bvv* zg9i`JPx{1k)9C2vS71u-39>)@0>G{&M=yF|G zjEs!zfNTru`9{{Vbm`K?!^6Y9>2x}m%jb*ZR?iIq%<*+k@$%?MvpY`K(j$(M53+;aVPLz`r1xg~62 z%W_%1dvl33a}BLritkc#DJrk`4|w0tInVR_@;RS#p3iwc=R7w(&NxB9>R0C7I$ z=(Rl~{#(1`wtGRSPd@;#3+U`8my`-!m!!k&Jg10oERt`BZe@So&& zVvxFR#Q^74T`wT>Wmi=QMOQD)KQ0s@u(h^!CcT-A-e{tKUM;Dqu7pify?#%cmr4Dh2h@EnP9+Mdv6nx` z5s-TeO#pFQ9ahu34K=#SF3rxshPyYHO)^OZ+Hf_z zG-P$`U%xGnC_5);V(pFgiJJ|Rv%qrIyxccgmzht7l1Es|4i8@7$P?lg!sk9W@qo{Ld9{h zoy?RPq9f2m=;;2ueJ+2s?IcRK1Ny+Hf)lKFXSj&W+*u*2jT$Y}9;WY@U;X-i8ADeh zAaY^6#X;av8Uk;=Xy*T8B=X6`3OoItM!q|^VYiUKqs+~CcU=x<2~}rrt&^00GwZT$ zbXa!D^2iFi>C48fPRF^uzsXs#GZR|Ha*+0e$%G39FL1xBI&1i42wL0gF8mFlK5$sL z^zyF}S);`Jxf#=k3QLwZ%P{gxI?G?^of91K%g71YXZiFS~W#NiNgi)e)2vB6lH5M9r&lyXS4D8#==z3{3cJMnbFsiPOC7`0e1Ad zOd!WUg{XLzAqnYf!rWo?ww`fQZmc5PnaH+T1HY_Np+nDH9*FV-rhwZVA6LhS9s+Z~ zBLM7pEe!6Mu5sQah-Vj&!V_VrFDfKH2kKBrG1r7ctyEzfHlDJ%5|9!g8}IJ0IoI-= zAvwyKDfuZyi%gm_cerBds)U16qrd|@)dP!hVIAerGDGvpLL2os=>=fMq%^_z@T zpt`;eBl!8PYl^k{0km?h;?DLy(4tus@*$-$?6tX`4q66pGf$$*-73Rn+H3lG2YoMC z>2^_evm{jymRpO6RQ&po6PO%(&J@Qs7>My0G${C}CS)pV<#FxwD=>*nej2troAY?= zQfMLB^Wi5C##r#GZ2z@z=4|V?-1S2n7MkWXqhb4s$#bg$mjzc)`Hx^t1NG9s{iN~d z^;l5;z4j&0z4H;(0Qq$67_Bq9px6C3h=l4X4WTbDzW>_}aF`@xOWUuZ9!Ln+*j5u0 z2lqi5ZI$^F0jpb#p?^ZCmnPno=c5yuXiE~Oy#IvU_mxnYxHrmqUbA-Vr%52lfX(|K z0uK_Z$@qw|w%ht*3zC0_W8R_y$Gg=Az*Ac5=Lb*P8XE><0^vCuSHw7P!f8e(+J#hw3@ zRV(Qs#7l@c7Z0sz3_&ETiH>E;WcF?^SM5!Ud+(Qu!%lol4;2%Y+Am+YH9$12O`fDea~7YI<2k|7vYU34 zv(e921%}*{zOGSx+XnlqG#`b8h}@e#k<+6Tle#)3UdsVuUO?>;J#u^Y}=64 zk}kNxtZ1@h!pFXAXr7&%8I*2E;172EKn(eqMF?AI{^tx`gKINcGD}s}BTUjyeARzf zqPkG0Gc~%r^+u_N=XPX?wnxE-tve7Cch=#^Ruk+m854SF8#=S4#KN#oWT(qDS?PP|!_6Ko^3urH|?+{=atm%tg3eh1%+ zQ(vXP2yRy~i5@To`ZPo=DhGu>vmP&)t8EwzVwAQbwirEZA8TS|zlHS;NnZ zefpN;Sa%dSeE*iJ>dz8F_ZWupAt7W$*GWiGwK#mn7Q$=z8}!`+?O7S_`OJx+>0PT_ zqZ`I?Mb%R}peL>dB&ecjRMU0GbybCHnw|ne$8ij3_lxG3PvB)--}qTo$y!Y#K>$Ey z*#v{Y3>^@=n+6?aSvBAByP~KB2pBH1K^S)}_X%c=viSnP(DwOz0J;}fTUJymnZR_7 zSTE|=xFuw0xcclm5^~%N-+OTm(cJR^eglBl`Q+KGq5}aFTp{qhA%U!N+RF z=GbuCByBox&l8(+t^ktMQfDhv84Zv%v~Pej7*Jg_4FK=9ncD4DEDz&W`e z3DpI@7zuM6w3Ou^CW%xk^-mj#vQh|?vz1@3Sd*JCSQi%gGPCn*<++gSYh| zuz0X&3XMjio9x(%#(7m$zl+X9J7&q?Hz(!fBnh3~DqXd3}ck4%4v{iNra8 zc9$EgM>?5|{5;F5C>w)C9tPG60Izc`_lF7Hc#6=PV}u?G5-O>M!Ox@!{R8y>$vcGB zcM+PDg^q8alt2{}tgucfCC&)m?}HnI2nyr8=8%ofWaG62O5j>lr*#10SH~>^Yd)|@ z_*r5qN$4wQ2>odr5NNYV>^etiB;}P8Y8itGSYbF22t44i0%w#?r)#hyW?0m{CU8`A zX0H`AdUQ;q&;5tcU!Ta6z!n&V-(4ia8A7*r6Z+(PK;YdxiM4Rwa6}d63A6(E0v#J0 zTjQ{TJv2$Od>jFS4`|#`-I05+u155q*MXKdov}!`Ey3lq=A=_joCJcu6e1-j zJM=XSE@t?hndt${`A~Z5)TzaiB#jgzz@yEFAM13}M*!eVo&;Lr@VGbHU_&`QFR;X$ zSBmEedrg(eVkxd+QlODYWOV1woe$;+T)K4WD`1-0LIilU!MJ{OlF*mVI!SDR_rMJ= zD2d6*O#&Soqmx`DW2;)e1jn5n`e`zm3^g`3F3l0>>+4&V$;vS_mxzF>I+KgcNT4NN zw2|1z5XcdBQK*@!Bf?U4x9QBZ2s#Lx&#C5$Npfd<0T;%yLwXh-wm# zMSyI_G@-xmby}jf%aW5M03~v4zp_JhfzPYMpwW0D-qh4ol_SvJ-u`u2mO~mhQ;8)p zu3Ui0$!!T!PMX-U-67UpcJTd$VC!l6KrEIB@7}$8S&qP#En6PUX0s#QOs3cSl(-(p zhDlFMiRFZr5cNkn{Z^1CjsQ6*!8|MPCIH!Zw5F!!i}reMQ9K^+=AC4=0r%OvdfIfF z&_7=QNAB^;>4?Cw{mPD7_H%igoZ*(uO?*h1nZJ zU;*%jjUQPgL~>GmmVUQQG{ zb%?SHk% zjmB;@G&FplCE#*NnUNto)l$U2cJr;w5NxjKp912_1@Cj*8IurkZ_ie&Sn)N8M#Hw_K2I&w zF|E-*U1S6piN9Wj%{dGltU-51=MF%6E`0$Tpt0HGhJOE}nNWyf}q+S!mx9tBQ2Fr3TQ zIU@}QgBJkXR}tXBg9jHDI+a_rYuBz-u)$X_>p^JMMQJ^r)#zlh^wVhxqYcPLRs|Xt z9ZSNZrP2~gA4-RqvSY`NYQzSGXGJMTi(d!?0;po#vdwc38p)&9| zCnhb3_05O{F_rw(hWP`U>f!U}&mYEt@FmNNE-LW+NcdNkViGtu%Eo$hCDWdsp7#O6 zHxP@j?u$I&SOjfZS6A2DsZ=_bQ1GBkP^UOZXqtajDLQq)=VqqE;t3qc2BIOZ{5KJU znQ=eOi`|GMmbSFCG(e7!+3}1aBEl_U)>KMqr_-3EKr$MJN;AvP&B@Kp&9C8{w*}rV zr>d%|yHSU*Z+LoWXs8eN4wxtGLI!NI}4y1Ke&a4wwl z6U(NMW5Zxb0#5=SJ87IfA8P_ z3XJ_Y&Vh6NU;c)`VlYbTOG}n4d1CM0y<7YH`#U3%NCH<-f=v)RL8GW;z)vkhQOcgP z;f@dbNi-6fOI)~cu@lF@v2e_i;{1GAgH3WTw1=R5d-dwoKi=EexDATIW{^QI?3hMi z+cyJxo|~Bs&CSk+W~Qd5MnHbO*r%~^-!{Al?tPSvaW9TpUtfPm{xgAO9$-Mw@gA$J zto+W(l`DU^YSpSAVZ+aO4S!>O9%A=YeCB`l5LpBx#lXP8-ONw8b@2ZWpJVNQcCCWx fy8`c-51s!Hzl@aQ*dJG?00000NkvXXu0mjfpQ$?R literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1e077104cd61e6a4c3707e87363b523077245a GIT binary patch literal 3981 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 nm0GElTB(&OnKI&V600000NkvXXu0mjff+~0a literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/toju-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..df0f15880bee46332dfc6622583215194f948b0f GIT binary patch literal 5036 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6cdf97c1196d48e9833487ff6de7c4cfc4e1232d GIT binary patch literal 6644 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 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/toju-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2960cbb6104b915c84760f889deed9bff2b3e17a GIT binary patch literal 9793 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 y3vD~Wi9;mL_v z7xo7KSWDz+M=Rp9m`_0a0lhg5l^&CC5BgTJ8~rS9SnxkU19>e|C7+YL@oW>oCNY~% zjVaC?;3_T_7YlcX;!4CHF6O@bK=w=3NCfVpder~+} zlN~`b;ZH0cqf)!anP6auHDwr2j1MwqivrM}D4y7zDN5QdIC*xfPfy#6Jpo~e>_)!x zF=AeZc`K$+Nj(iiSI|EYm%+91t@RkYEp50Fb$oH(=LnB0Hd9b~x58vt)(rmXq3PpfJJ>#E_a`eP*G4lhV92sh zbXI2&wZHc?FWR^m_YsE(f7^=(i6UMrRF*M&!zdenDyv*bN_8H}5an{^P4zNHY#6Tc z)i+L?j3t%eX4Pxkzr0EOY6u%Yw-NUUc7AtfIdW55h;Jyg-dLN;WU>ksc+OcyXo`lC z7t7Lo;jdSB>`k{Tu+2Qoi$S<)+2d+3ego@0V>ovAG7H>!ZiBZr&|Km6Y7yKi2u#sCQd>&w_Q3pqS1D z-u_vP59v|e5QWMs4JWNB1<&9`pitC0?-hGt^o=9MN|SAN;>>mC(6dDQ?qRscdK0xR zRC8J#l=GdZxX$mu7hD0=OaTt?Db(*>rQs8?DZUsFU~AHJ(d>`8X&FWaz4E5#71%o~ zG!%o-pSid?x9j!j<%0xt3IZ)W#yxO)`Z26OLF?3w8Smx{(dRtVwSjC6pWR<8JBs!=ZyZkxJ{N9*89VkN99f3 zf8(oE=#s4|gZS%J^UNK8`}h`+20jJsUq)b_%ZoHn{7$NKG9EQ`|D)ne{G_ku@gthG z7-niKMeVIfPMrgZ^`Odpm9XhGjd70CO&si_R!5kuWvT1swi6GK8Nd6a)V1wg3?hQ5zy87yG)A z>l-%CDZkTXe+WkeA;GuOM%rG42n1ieIJ&c}9~w~aRpNB-RAO!-(wEVm7P>QhNZIt9 zndan|MWquZpErNAp~;O&i~IC6nA%BJr1fHlL73Dnjt)Z?*MCp&>itYcar+@PsFoJQ25T5$R_O_runRf zYy9wp{K7O-l zu4%*E(S3+5s8jzT+L?W`tTU-n9S68ayCHt+h$L)#VDMqai7Kyrb&%z5`@d4GbEt(Z{6Zc@HeGjn zLzkc{rsU>1kvipF;X_O2`5T9vc&E)Dm*A%{8uRYdeIzn}-VuExvbm@fYRkFkIl&xZRR~3+zu%pCw*v+hop9YfG{} z9PAo5)z%ejk?(5~{={nIJl@jQGE58oA)Ss(?i$_@lgl9a&?qA@gC$S(@)(+|l0EDp zkUIZKtOp6gkuoN4TC{3j#SPIT?1G}o!meIU&1yA6z~`SSjCePLfo`6EG4@Q^SQYfD zME-0j+%@=0&VAfw9Y5_WvMGliaFPk{%3FI|b;jusr~AH|8USTRd?k|cV2vLq^4mng zBbK{jx34U{b->g2b00EjxS8GQ_8h7{Qbfh@&CMXrJ_&~3hxHXZFPckU?^m_Tk7p*8J@@0Wc zuu-6B0IE(`72?ZVQ1o+uu6Tt(J6kKmN#r$X$kw_rmnnLAU#`6i6oL%RENvDm&`rL# zyZ5li(X6^=9icG~lp41ln{gubrnhOKkIMm7%PH7ra1u(d*@ zi7@&!+4?ug=pS|)13s*#$&ScZ=QtY8*gnKe`W7r){B4m_IhMf$pzK1hdLelO$4@=4 zS8%erVQyIhZnGiN;})kh`1eHIR4@$Fi%v5EV-#5kn$z?mDfc}*#EGjB zk|sx5I+_c$Ns0pFYfl?j+AP!x+p^YFJG=zYj&2a?DnIj$ zccEWAlL4$A-Y2$CSf8FuqiO%K@f~LyRmujzNZNnLX!ozOD8cxscX0zfP&` z2)d){2xXh#3mu89_Tfw&hppPL0IF>n;~o}~40wK_+lq@+RytODW3p;vfkEH1#2goi z8a2eR<;Z)3Oq*)~k}$09S>op_FRxlr(VACElefuBp6qZRUQ)f}9AlV{%Rm-l*ZW2KEg@mTk^vReLa*bRHHFPUPx=o?my9TrfCXdx8Y&v4yLVmFisW}xDSJQ!PQli}Vv;xwdP;f8` zq;@jO2TtV9{TYgQIuQ;4`PRd*F_@6(X*bhzK;L$@e)Qr8`sr&dXHDVuq{ywl31V%7 zkeCB6O0TA`187$ofjRC5e=T zKBG1a0d2dhJZN=}v&a42h(nE8kadY8Zyz>ApKv)I^Zw22} zS@CK|(l#2SX?XY^_HHGAVV**d1gRWVF7HM)ZDd+sZoZV9cz_ddL`TjHme`^YMkz{cKKujz6<=Z#HSOMb( z^OGCAXi;9c6NAS*{pBcYmA7H4D3O9J3Zl2>{$+9?nxrNPtDSSw=Xa&7m%gS+?mc<7 z>XPo{t?R`Jm3n}zo*n9I%uVBD0k~%__$8Y9EoZ8v4Y)6H0uGuL-+s{6I{&3=HiCX8 zrYb#tgQ-PH_^s7v&# z2^ubzq(}w5|8pH_eu(#*v}EUWG0zDuczM-()4YJ-Q_soMz72`bABz6GbA?l4xnO(V z(^{Qi@4rkBos7DTLo;4+7(WkM!O+|w=vm{UJag(GF~^ZReVUW^y6azP2H2oo*5pI# zK6bDpFEy0Je$Ifs{~w1Iy`gDX<*@Vu4H@eB!E^F_Rd7HVsqrjnGdC9e>WFIrv;-wl zu&w5Z#GuV@)AbEO5=$~6smh0{eUAcp(Ye2DpaKP`oW2+Mr=;4jIQeVG?;{D~km4-q zYo5p|#mL~&xP0-dc~PX9H;H$k!a^(}Decp{GCX5i3dR=!WGI$C*-jUdd*|o#?FqRgbfvEmou58aq2W`d=Tm~J1>USN(f{?bSNgu* zOp|?QwOVBbDPM~Q&eWnL#36m+uoZNkSeM}S7VPKtcf_2-?RVm`tCOI+ zcVz8*+tVlaqIjkKqKczlU9CILo2C24&nl<*)$a+M8E5H@*laf|uO9*c<0NSf3&l2zAwUefgo5=tHQyaEF>fUvB!`Qyb7Y7_!;i#tin-6rPyMhMo35{qyYAul zzqSn5C^nN>uqM@eqfCS?b(@m7;~BIs2sX$_+r(C*@5O3TaLnU`D(?= zIPV{WdulsgNaITPZ1_C)d~`{tvMCpfI#=(;;kd#4{NYp*wd?QLh)3PaG$7^eM_7q1 zWM`zNk_o#<7xszE)zP^#dT3Fwe7@?;*=58RSAB;{D^>HBb1pwtI!FwD=I^2Gl8sNd zB{^G(c|Te%yeI)!we4qsI_iFa#6OqMOc#rT5pSyip(>}6P!uu)Kb_o%I6AU8G@uA@ zAyi>m|A8Fpod4-16No_j;>6;81Swgw;?*EjNN24eRe*L^igbgA_Dv|>QFH`o+DGY> zbAE#?YpD)FQ#6(*M6uw=e(}FRLv@*O4+oDuoa$-Ne-T+O2feT_dqz7c!iKXAHeli# zbdOqI+FBr11cls9uCpF8`$>S{QPb>5VyOi}`#4N$NzijET9trixd&1e-VoGW`WrJ^ zMn|v7$t`Y?o3FMjqNG;-_voJUmt2Oc%UO~y!2cv8O9&11;}>7f=d48{J+}Pi$n?KL zDLTS0Mv_ySccwQ~Tei|zL)>K{VZ|HzhcH6xvZ4#)5-4BYiV|24Y_wJ}%>hApUGJm? z?@a9h1HHK2JsbP>qQ&zZG-D%HGM#3^V@A}8s43#}K^yuY((yP;4LD&Eq3Y=7IY*QT z+%#3oaCJ+@&vDBv0gpkvB3Pq)H)?q`SR5>k)zFk#Y0 z)*OIyPDPlO!48$I{#2%~1^D%7T-`lAbapZK?@whU_QM(y|L5Ro3;0tfKHxtBp2(=X zrvvyYB~r!Ct2WK#=w)Svfbw z44Cf^yEV06QE<01NEKOpFm-GOJ@JWPF4IwZ?*SKRiWPyTcqNT-p5wSPY{CEMmow;#v(0pX)_K5-W0edv=Pi4pBMF`+3P@!- zUd@FLfiCBhdS_(BkqLtlu`q8O9T1%zrdSM+UVSqc=LinJni z#0bUiQ4EYMi+2=Z{1XP0BQU4GgH2Eg;0&n>w~-Y2&M9I?aN`ABE+Gy4f}hs}3BO;- zPRGB|Qq!vCb^-rLuXAlIT#1xtW1vfNk~0A^6AqEJRwxlt#J5D8(pi&6=UfgjxxtjL zXdWb)ISVZj>;8^J>FZSH+@BFWvDhC`nEH>nW3WQvK@sZaOV4dZZFYu9Dvq%9XhjL) zT&h7h!UJlkzl^d@$+qT}%Xsrfv)^QNc5Lmv<4f*fQmcTr2l%r+(WE0T53>m)Y>&At z&aHTynvej_oHbIM!2>ko@2plJxlj}=-(zMJ8LA5SX+k|`Tw%(aRZX)j+RstYL5%}= zRI}CjTxR}7pdVxO?Do|&Ukf@Z!7+}E}7XmlhL? z+o`}*@Mmd2)dZ>OScGSUnK|$loE3nwUgRc61DV~U$d`Ww-4c`8F(3%Dwc6Yl!$9pd%l-oVOpXE4mPB`PIR({&N?rZ;!VThbE!2&OieN|3x@Te%lhvz8CX7%Ls& zP8U-t(NLRW!jKpo^w$-l!tyE;BD>*B!KEEHbU|i61kR2*FYN?JH}bkRh%x6DcJ_^4 zUvj;8Q>8SH#yF1X2Jn=eSU?)#>t2g}B2~}`ZKu13IoJ91`A?7V$(e17%ewQWF-oJi z?tlw~ScDE*zWi@mQ2^l-+yI(SGNuZ3$B4*k&P#PB<*_yIrxJ!Zdb3XbwCH;_uGobw zX4OWA+an{LTMTZAdA|(*Q*we8GQR@pSou=W$D-+=_ja)FN~?zbI96I{&;mm2?7y!I zrxBI8gIzGaPMPyyCti|zUhJS7}t$>Q9<3LvIDE+q1y;J@&Ci6*m?7gs{K*%`ng_Hc=&_{@x@jLZ4TVu=W+4>0Ed3~t^fc4 literal 0 HcmV?d00001 diff --git a/toju-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/toju-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..a40d73e9c68ea638c28c7b13e4bcd1e179a3a102 GIT binary patch literal 15916 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 + + #FFFFFF + \ No newline at end of file diff --git a/toju-app/android/app/src/main/res/values/strings.xml b/toju-app/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5ea1bd5 --- /dev/null +++ b/toju-app/android/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + MetoYou + MetoYou + com.metoyou.app + com.metoyou.app + Voice calls and microphone access + diff --git a/toju-app/android/app/src/main/res/values/styles.xml b/toju-app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/toju-app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/toju-app/android/app/src/main/res/xml/file_paths.xml b/toju-app/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/toju-app/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/toju-app/android/app/src/main/res/xml/network_security_config.xml b/toju-app/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..db800e8 --- /dev/null +++ b/toju-app/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/toju-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/toju-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/toju-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/toju-app/android/build.gradle b/toju-app/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/toju-app/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/toju-app/android/capacitor.settings.gradle b/toju-app/android/capacitor.settings.gradle new file mode 100644 index 0000000..08c11ad --- /dev/null +++ b/toju-app/android/capacitor.settings.gradle @@ -0,0 +1,24 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') + +include ':capacitor-community-sqlite' +project(':capacitor-community-sqlite').projectDir = new File('../../node_modules/@capacitor-community/sqlite/android') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android') + +include ':capacitor-camera' +project(':capacitor-camera').projectDir = new File('../../node_modules/@capacitor/camera/android') + +include ':capacitor-device' +project(':capacitor-device').projectDir = new File('../../node_modules/@capacitor/device/android') + +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') + +include ':capacitor-local-notifications' +project(':capacitor-local-notifications').projectDir = new File('../../node_modules/@capacitor/local-notifications/android') + +include ':capacitor-push-notifications' +project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/android') diff --git a/toju-app/android/gradle.properties b/toju-app/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/toju-app/android/gradle.properties @@ -0,0 +1,22 @@ +# 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. +org.gradle.jvmargs=-Xmx1536m + +# 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 diff --git a/toju-app/android/gradle/wrapper/gradle-wrapper.jar b/toju-app/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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="\\\"\\\"" + + +# 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/toju-app/android/gradlew.bat b/toju-app/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/toju-app/android/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/toju-app/android/settings.gradle b/toju-app/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/toju-app/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/toju-app/android/variables.gradle b/toju-app/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/toju-app/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/toju-app/capacitor.config.ts b/toju-app/capacitor.config.ts new file mode 100644 index 0000000..aeb0528 --- /dev/null +++ b/toju-app/capacitor.config.ts @@ -0,0 +1,30 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'com.metoyou.app', + appName: 'MetoYou', + webDir: '../dist/client/browser', + server: { + androidScheme: 'https' + }, + plugins: { + SystemBars: { + insetsHandling: 'css', + style: 'DARK' + }, + LocalNotifications: { + smallIcon: 'ic_stat_icon_config_sample', + iconColor: '#488AFF', + sound: 'call.wav' + }, + PushNotifications: { + presentationOptions: [ + 'badge', + 'sound', + 'alert' + ] + } + } +}; + +export default config; diff --git a/toju-app/ios/.gitignore b/toju-app/ios/.gitignore new file mode 100644 index 0000000..f470299 --- /dev/null +++ b/toju-app/ios/.gitignore @@ -0,0 +1,13 @@ +App/build +App/Pods +App/output +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins + +# Generated Config files +App/App/capacitor.config.json +App/App/config.xml diff --git a/toju-app/ios/App/App.xcodeproj/project.pbxproj b/toju-app/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d2fbc98 --- /dev/null +++ b/toju-app/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,376 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + ); + path = App; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + packageProductDependencies = ( + 4D22ABE82AF431CB00220026 /* CapApp-SPM */, + ); + productName = App; + productReference = 504EC3041FED79650016851F /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + packageReferences = ( + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */, + ); + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = com.metoyou.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.metoyou.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "CapApp-SPM"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4D22ABE82AF431CB00220026 /* CapApp-SPM */ = { + isa = XCSwiftPackageProductDependency; + package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */; + productName = "CapApp-SPM"; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/toju-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/toju-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/toju-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/toju-app/ios/App/App/AppDelegate.swift b/toju-app/ios/App/App/AppDelegate.swift new file mode 100644 index 0000000..c3cd83b --- /dev/null +++ b/toju-app/ios/App/App/AppDelegate.swift @@ -0,0 +1,49 @@ +import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + +} diff --git a/toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..adf6ba01dbe256605c5152ac1fd78ae99aaa2a8d GIT binary patch literal 110522 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 C`SD)> literal 0 HcmV?d00001 diff --git a/toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9b7d382 --- /dev/null +++ b/toju-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon-512@2x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/toju-app/ios/App/App/Assets.xcassets/Contents.json b/toju-app/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/toju-app/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 0000000..d7d96a6 --- /dev/null +++ b/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "splash-2732x2732-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png b/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png new file mode 100644 index 0000000000000000000000000000000000000000..33ea6c970f2df1db62a624a55e5bbcc4ee07bbdf GIT binary patch literal 41273 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 literal 0 HcmV?d00001 diff --git a/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png b/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png new file mode 100644 index 0000000000000000000000000000000000000000..33ea6c970f2df1db62a624a55e5bbcc4ee07bbdf GIT binary patch literal 41273 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 literal 0 HcmV?d00001 diff --git a/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png b/toju-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png new file mode 100644 index 0000000000000000000000000000000000000000..33ea6c970f2df1db62a624a55e5bbcc4ee07bbdf GIT binary patch literal 41273 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 literal 0 HcmV?d00001 diff --git a/toju-app/ios/App/App/Base.lproj/LaunchScreen.storyboard b/toju-app/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..e7ae5d7 --- /dev/null +++ b/toju-app/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toju-app/ios/App/App/Base.lproj/Main.storyboard b/toju-app/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 0000000..b44df7b --- /dev/null +++ b/toju-app/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/toju-app/ios/App/App/Info.plist b/toju-app/ios/App/App/Info.plist new file mode 100644 index 0000000..c3643ea --- /dev/null +++ b/toju-app/ios/App/App/Info.plist @@ -0,0 +1,57 @@ + + + + + CAPACITOR_DEBUG + $(CAPACITOR_DEBUG) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + MetoYou + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIBackgroundModes + + audio + remote-notification + voip + + + diff --git a/toju-app/ios/App/App/MetoyouMobilePlugin.swift b/toju-app/ios/App/App/MetoyouMobilePlugin.swift new file mode 100644 index 0000000..9a337d4 --- /dev/null +++ b/toju-app/ios/App/App/MetoyouMobilePlugin.swift @@ -0,0 +1,92 @@ +import Foundation +import Capacitor +import CallKit + +@objc(MetoyouMobilePlugin) +public class MetoyouMobilePlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "MetoyouMobilePlugin" + public let jsName = "MetoyouMobile" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "startCallKitSession", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "endCallKitSession", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "isRemotePushConfigured", returnType: CAPPluginReturnPromise) + ] + + private let provider: CXProvider + private let callController = CXCallController() + private var activeCallUuid: UUID? + + public override init() { + let configuration = CXProviderConfiguration(localizedName: "MetoYou") + configuration.supportsVideo = true + configuration.maximumCallsPerCallGroup = 1 + configuration.supportedHandleTypes = [.generic] + provider = CXProvider(configuration: configuration) + super.init() + provider.setDelegate(self, queue: nil) + } + + @objc func startCallKitSession(_ call: CAPPluginCall) { + guard let callId = call.getString("callId"), + let displayName = call.getString("displayName") else { + call.reject("Missing callId/displayName") + return + } + + let uuid = UUID(uuidString: callId) ?? UUID() + activeCallUuid = uuid + + let handle = CXHandle(type: .generic, value: displayName) + let startAction = CXStartCallAction(call: uuid, handle: handle) + startAction.isVideo = false + let transaction = CXTransaction(action: startAction) + + callController.request(transaction) { error in + if let error = error { + call.resolve(["supported": false, "error": error.localizedDescription]) + return + } + + let update = CXCallUpdate() + update.remoteHandle = handle + update.localizedCallerName = displayName + update.hasVideo = false + self.provider.reportCall(with: uuid, updated: update) + call.resolve(["supported": true]) + } + } + + @objc func endCallKitSession(_ call: CAPPluginCall) { + guard let uuid = activeCallUuid else { + call.resolve() + return + } + + let endAction = CXEndCallAction(call: uuid) + let transaction = CXTransaction(action: endAction) + + callController.request(transaction) { _ in + self.activeCallUuid = nil + call.resolve() + } + } + + @objc func isRemotePushConfigured(_ call: CAPPluginCall) { + call.resolve(["configured": true]) + } +} + +extension MetoyouMobilePlugin: CXProviderDelegate { + public func providerDidReset(_ provider: CXProvider) { + activeCallUuid = nil + } + + public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + activeCallUuid = nil + action.fulfill() + } +} diff --git a/toju-app/ios/App/CapApp-SPM/.gitignore b/toju-app/ios/App/CapApp-SPM/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/toju-app/ios/App/CapApp-SPM/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/toju-app/ios/App/CapApp-SPM/Package.swift b/toju-app/ios/App/CapApp-SPM/Package.swift new file mode 100644 index 0000000..dcbf975 --- /dev/null +++ b/toju-app/ios/App/CapApp-SPM/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version: 5.9 +import PackageDescription + +// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands +let package = Package( + name: "CapApp-SPM", + platforms: [.iOS(.v15)], + products: [ + .library( + name: "CapApp-SPM", + targets: ["CapApp-SPM"]) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.4.0"), + .package(name: "CapacitorCommunitySqlite", path: "../../../../node_modules/@capacitor-community/sqlite"), + .package(name: "CapacitorApp", path: "../../../../node_modules/@capacitor/app"), + .package(name: "CapacitorCamera", path: "../../../../node_modules/@capacitor/camera"), + .package(name: "CapacitorDevice", path: "../../../../node_modules/@capacitor/device"), + .package(name: "CapacitorFilesystem", path: "../../../../node_modules/@capacitor/filesystem"), + .package(name: "CapacitorLocalNotifications", path: "../../../../node_modules/@capacitor/local-notifications"), + .package(name: "CapacitorPushNotifications", path: "../../../../node_modules/@capacitor/push-notifications"), + .package(name: "CapgoCapacitorAudioSession", path: "../../../../node_modules/@capgo/capacitor-audio-session") + ], + targets: [ + .target( + name: "CapApp-SPM", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm"), + .product(name: "CapacitorCommunitySqlite", package: "CapacitorCommunitySqlite"), + .product(name: "CapacitorApp", package: "CapacitorApp"), + .product(name: "CapacitorCamera", package: "CapacitorCamera"), + .product(name: "CapacitorDevice", package: "CapacitorDevice"), + .product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"), + .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"), + .product(name: "CapacitorPushNotifications", package: "CapacitorPushNotifications"), + .product(name: "CapgoCapacitorAudioSession", package: "CapgoCapacitorAudioSession") + ] + ) + ] +) diff --git a/toju-app/ios/App/CapApp-SPM/README.md b/toju-app/ios/App/CapApp-SPM/README.md new file mode 100644 index 0000000..03964db --- /dev/null +++ b/toju-app/ios/App/CapApp-SPM/README.md @@ -0,0 +1,5 @@ +# CapApp-SPM + +This package is used to host SPM dependencies for your Capacitor project + +Do not modify the contents of it or there may be unintended consequences. diff --git a/toju-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift b/toju-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift new file mode 100644 index 0000000..945afec --- /dev/null +++ b/toju-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift @@ -0,0 +1 @@ +public let isCapacitorApp = true diff --git a/toju-app/ios/debug.xcconfig b/toju-app/ios/debug.xcconfig new file mode 100644 index 0000000..53ce18d --- /dev/null +++ b/toju-app/ios/debug.xcconfig @@ -0,0 +1 @@ +CAPACITOR_DEBUG = true diff --git a/toju-app/package.json b/toju-app/package.json new file mode 100644 index 0000000..41c0d97 --- /dev/null +++ b/toju-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@metoyou/client", + "private": true, + "version": "1.0.0", + "description": "MetoYou Angular product client and Capacitor shell", + "dependencies": { + "@capacitor-community/sqlite": "^8.1.0", + "@capacitor/android": "^8.4.0", + "@capacitor/app": "^8.1.0", + "@capacitor/camera": "^8.2.0", + "@capacitor/core": "^8.4.0", + "@capacitor/device": "^8.0.2", + "@capacitor/filesystem": "^8.1.2", + "@capacitor/ios": "^8.4.0", + "@capacitor/local-notifications": "^8.2.0", + "@capacitor/push-notifications": "^8.1.1", + "@capgo/capacitor-audio-session": "^8.0.40" + } +} diff --git a/toju-app/src/app/app.html b/toju-app/src/app/app.html index 64666a8..77c6eb8 100644 --- a/toju-app/src/app/app.html +++ b/toju-app/src/app/app.html @@ -1,6 +1,6 @@

void) | null = null; private themeStudioControlsDragOffset: { x: number; y: number } | null = null; private themeStudioControlsBounds: { width: number; height: number } | null = null; @@ -331,6 +339,9 @@ export class App implements OnInit, OnDestroy { } void this.notifications.initialize().catch(() => {}); + void this.mobilePersistence.initialize().catch(() => {}); + void this.mobileLifecycle.initialize().catch(() => {}); + this.mobileCallSession.initialize(); void this.setupDesktopDeepLinks().catch(() => {}); this.userStatus.start(); diff --git a/toju-app/src/app/core/platform/platform.service.ts b/toju-app/src/app/core/platform/platform.service.ts index 549634f..42bbbdc 100644 --- a/toju-app/src/app/core/platform/platform.service.ts +++ b/toju-app/src/app/core/platform/platform.service.ts @@ -1,15 +1,23 @@ import { Injectable, inject } from '@angular/core'; +import { detectRuntimePlatform, isCapacitorNativeRuntime } from '../../infrastructure/mobile/logic/platform-detection.rules'; import { ElectronBridgeService } from './electron/electron-bridge.service'; @Injectable({ providedIn: 'root' }) export class PlatformService { readonly isElectron: boolean; + readonly isCapacitor: boolean; readonly isBrowser: boolean; private readonly electronBridge = inject(ElectronBridgeService); constructor() { this.isElectron = this.electronBridge.isAvailable; - this.isBrowser = !this.isElectron; + const runtime = detectRuntimePlatform({ + hasElectronApi: this.isElectron, + capacitorIsNative: isCapacitorNativeRuntime() + }); + + this.isCapacitor = runtime === 'capacitor'; + this.isBrowser = runtime === 'browser'; } } diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html index a02e90f..78b4510 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html @@ -155,53 +155,147 @@ } - @if (klipyEnabled()) { - - } - -
- - - @if (showEmojiPicker()) { -
- +
+ + + @if (showComposerMediaMenu()) { + +
+ @for (option of composerMediaMenuOptions(); track option.action) { + + } +
+
+ } + + @if (showEmojiPicker()) { + + + + } +
+ } @else { + @if (shouldShowAttachmentButton()) { + } -
+ + @if (klipyEnabled()) { + + } + +
+ + + @if (showEmojiPicker()) { +
+ +
+ } +
+ } - } -
+
+ @if (!inline()) { +
+

Emoji

+ @if (compact()) { + + } +
+ }
diff --git a/toju-app/src/app/features/direct-call/private-call.component.ts b/toju-app/src/app/features/direct-call/private-call.component.ts index 0494f37..061d81b 100644 --- a/toju-app/src/app/features/direct-call/private-call.component.ts +++ b/toju-app/src/app/features/direct-call/private-call.component.ts @@ -43,6 +43,7 @@ import { import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from '../../domains/voice-session'; import { ScreenShareQualityDialogComponent } from '../../shared'; import { ViewportService } from '../../core/platform'; +import { MobileMediaService, MobilePlatformService } from '../../infrastructure/mobile'; import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selectors'; import { UsersActions } from '../../store/users/users.actions'; import { User } from '../../shared-kernel'; @@ -87,11 +88,15 @@ export class PrivateCallComponent { private readonly playback = inject(VoicePlaybackService); private readonly screenShare = inject(ScreenShareFacade); private readonly viewport = inject(ViewportService); + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly mobileMedia = inject(MobileMediaService); private chatResizing = false; readonly allUsers = this.store.selectSignal(selectAllUsers); readonly currentUser = this.store.selectSignal(selectCurrentUser); readonly isMobile = this.viewport.isMobile; + readonly showSpeakerphoneButton = computed(() => this.mobilePlatform.isNativeMobile()); + readonly speakerphoneEnabled = signal(true); readonly callIdInput = input(null); readonly overlayMode = input(false); readonly routeCallId = toSignal(this.route.paramMap.pipe(map((params) => params.get('callId'))), { @@ -342,6 +347,13 @@ export class PrivateCallComponent { this.broadcastLocalVoiceState(); } + async toggleSpeakerphone(): Promise { + const nextEnabled = !this.speakerphoneEnabled(); + + this.speakerphoneEnabled.set(nextEnabled); + await this.mobileMedia.setSpeakerphoneEnabled(nextEnabled); + } + toggleDeafen(): void { const nextDeafened = !this.isDeafened(); diff --git a/toju-app/src/app/features/room/voice-workspace/voice-workspace-stream-tile/voice-workspace-stream-tile.component.ts b/toju-app/src/app/features/room/voice-workspace/voice-workspace-stream-tile/voice-workspace-stream-tile.component.ts index 4b8bf74..e500053 100644 --- a/toju-app/src/app/features/room/voice-workspace/voice-workspace-stream-tile/voice-workspace-stream-tile.component.ts +++ b/toju-app/src/app/features/room/voice-workspace/voice-workspace-stream-tile/voice-workspace-stream-tile.component.ts @@ -25,6 +25,10 @@ import { import { UserAvatarComponent } from '../../../../shared'; import { ViewportService } from '../../../../core/platform'; +import { + MobileAppLifecycleService, + MobilePictureInPictureService +} from '../../../../infrastructure/mobile'; import { VoiceWorkspacePlaybackService } from '../voice-workspace-playback.service'; import { VoiceWorkspaceStreamItem } from '../voice-workspace.models'; @@ -55,6 +59,8 @@ import { VoiceWorkspaceStreamItem } from '../voice-workspace.models'; export class VoiceWorkspaceStreamTileComponent implements OnDestroy { private readonly workspacePlayback = inject(VoiceWorkspacePlaybackService); private readonly viewport = inject(ViewportService); + private readonly mobileLifecycle = inject(MobileAppLifecycleService); + private readonly mobilePictureInPicture = inject(MobilePictureInPictureService); private fullscreenHeaderHideTimeoutId: ReturnType | null = null; readonly item = input.required(); @@ -74,6 +80,10 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy { readonly muted = signal(false); constructor() { + void this.mobileLifecycle.initialize(); + this.mobileLifecycle.onAppStateChange((isActive) => { + void this.handleAppStateChange(isActive); + }); effect(() => { const ref = this.videoRef(); const item = this.item(); @@ -150,6 +160,7 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy { ngOnDestroy(): void { this.clearFullscreenHeaderHideTimeout(); + void this.mobilePictureInPicture.exit(); const tile = this.tileRef()?.nativeElement; @@ -160,6 +171,24 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy { this.unlockOrientation(); } + private async handleAppStateChange(isActive: boolean): Promise { + if (isActive || !this.focused() || !this.mobilePictureInPicture.isSupported()) { + if (isActive) { + await this.mobilePictureInPicture.exit(); + } + + return; + } + + const video = this.videoRef()?.nativeElement; + + if (!video || !this.item().stream) { + return; + } + + await this.mobilePictureInPicture.enter(video); + } + canToggleFullscreen(): boolean { return !this.mini() && !this.compact(); } diff --git a/toju-app/src/app/infrastructure/mobile/README.md b/toju-app/src/app/infrastructure/mobile/README.md new file mode 100644 index 0000000..ec30a23 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/README.md @@ -0,0 +1,27 @@ +# Mobile infrastructure + +Loosely coupled Capacitor/native bridge for the Angular product client. Domains depend on facades in this folder — never on `@capacitor/*` imports directly. + +## Facades + +| Service | Responsibility | +|---------|----------------| +| `MobilePlatformService` | Runtime detection (`browser` / `capacitor` / `electron`) and mobile UX flags | +| `MobileNotificationsService` | Local/push notifications for calls | +| `MobileCallSessionService` | In-call notification actions, background audio session, stream video hand-off | +| `MobileMediaService` | Attachment picker, speakerphone route, screen-share/PiP capability probes | +| `MobilePictureInPictureService` | Stream pop-out while backgrounded | +| `MobilePersistenceService` | Native SQLite schema init (`@capacitor-community/sqlite`) | +| `MobileSqliteConnectionService` | Shared SQLite connection for persistence + `DatabaseService` | +| `MobileCallKitService` | iOS CallKit active-call reporting for background voice | +| `MobilePushRegistrationService` | FCM/APNs token registration with signaling server; skips `PushNotifications.register()` when Firebase/APNs is not configured | +| `MobileAppLifecycleService` | Foreground/background lifecycle | + +## Adapters + +- `adapters/web/*` — browser fallbacks (Notification API, hidden file input, Document PiP). +- `adapters/capacitor/*` — lazy-loaded Capacitor plugins via `capacitor-plugin-loader.ts`. + +## Rules + +Pure platform/call-notification rules live in `logic/*.rules.ts` and are Vitest-tested without Angular. diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-app-lifecycle.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-app-lifecycle.adapter.ts new file mode 100644 index 0000000..c567912 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-app-lifecycle.adapter.ts @@ -0,0 +1,23 @@ +import type { MobileAppLifecycleAdapter } from '../../contracts/mobile.contracts'; +import { loadCapacitorAppPlugin } from './capacitor-plugin-loader'; + +/** Capacitor App plugin lifecycle bridge. */ +export class CapacitorMobileAppLifecycleAdapter implements MobileAppLifecycleAdapter { + private handler: ((isActive: boolean) => void) | null = null; + + async initialize(): Promise { + const App = loadCapacitorAppPlugin(); + + if (!App) { + return; + } + + await App.addListener('appStateChange', (state) => { + this.handler?.(state.isActive); + }); + } + + onAppStateChange(handler: (isActive: boolean) => void): void { + this.handler = handler; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-callkit.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-callkit.adapter.ts new file mode 100644 index 0000000..0988658 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-callkit.adapter.ts @@ -0,0 +1,25 @@ +import type { MobileCallKitAdapter } from '../../contracts/mobile.contracts'; +import { MetoyouMobile } from './metoyou-mobile.plugin'; + +/** iOS CallKit bridge via the MetoyouMobile native plugin. */ +export class CapacitorMobileCallKitAdapter implements MobileCallKitAdapter { + async startActiveCall(callId: string, displayName: string): Promise { + try { + const result = await MetoyouMobile.startCallKitSession({ callId, displayName }); + + if (!result.supported) { + console.info('[mobile] CallKit is unavailable on this iOS build'); + } + } catch (error) { + console.info('[mobile] CallKit start skipped', error); + } + } + + async endActiveCall(callId: string): Promise { + try { + await MetoyouMobile.endCallKitSession({ callId }); + } catch (error) { + console.info('[mobile] CallKit end skipped', error); + } + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-media.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-media.adapter.ts new file mode 100644 index 0000000..143b9a1 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-media.adapter.ts @@ -0,0 +1,68 @@ +import type { MobileMediaAdapter } from '../../contracts/mobile.contracts'; +import { loadCapacitorAudioSessionPlugin } from './capacitor-plugin-loader'; +import { MetoyouMobile } from './metoyou-mobile.plugin'; +import { WebMobileMediaAdapter } from '../web/web-mobile-media.adapter'; + +/** Capacitor media adapter with native speaker routing and background voice session hooks. */ +export class CapacitorMobileMediaAdapter extends WebMobileMediaAdapter implements MobileMediaAdapter { + private backgroundSessionActive = false; + + override async setSpeakerphoneEnabled(enabled: boolean): Promise { + try { + await MetoyouMobile.setSpeakerphoneEnabled({ enabled }); + return; + } catch { + // Android plugin unavailable in web builds; fall through to iOS audio session. + } + + const AudioSession = loadCapacitorAudioSessionPlugin(); + + if (!AudioSession) { + return; + } + + await AudioSession.overrideOutput(enabled ? 'speaker' : 'default'); + } + + override async startBackgroundAudioSession(): Promise { + if (this.backgroundSessionActive) { + return; + } + + this.backgroundSessionActive = true; + + try { + await MetoyouMobile.startVoiceForegroundService(); + } catch (error) { + console.info('[mobile] background voice foreground service unavailable', error); + } + } + + override async stopBackgroundAudioSession(): Promise { + if (!this.backgroundSessionActive) { + return; + } + + this.backgroundSessionActive = false; + + try { + await MetoyouMobile.stopVoiceForegroundService(); + } catch (error) { + console.info('[mobile] failed to stop background voice session', error); + } + } + + override isScreenShareSupported(): boolean { + const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : ''; + + if (/iPhone|iPad|iPod/i.test(userAgent)) { + return false; + } + + return !!navigator.mediaDevices?.getDisplayMedia; + } + + override isPictureInPictureSupported(): boolean { + return super.isPictureInPictureSupported(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-notifications.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-notifications.adapter.ts new file mode 100644 index 0000000..94946cb --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-notifications.adapter.ts @@ -0,0 +1,151 @@ +import type { CallNotificationActionIntent, CallNotificationPayload } from '../../logic/call-notification.rules'; +import { resolveCallNotificationAction } from '../../logic/call-notification.rules'; +import type { MobileNotificationAdapter } from '../../contracts/mobile.contracts'; +import { loadCapacitorLocalNotificationsPlugin, loadCapacitorPushNotificationsPlugin } from './capacitor-plugin-loader'; + +const INCOMING_CALL_CHANNEL_ID = 'toju-incoming-call'; +const ACTIVE_CALL_CHANNEL_ID = 'toju-active-call'; + +/** Capacitor local + push notification bridge with action buttons for in-call controls. */ +export class CapacitorMobileNotificationsAdapter implements MobileNotificationAdapter { + private actionHandler: ((input: { callId: string; intent: CallNotificationActionIntent }) => void) | null = null; + private listenersRegistered = false; + + async initialize(): Promise { + const LocalNotifications = loadCapacitorLocalNotificationsPlugin(); + const PushNotifications = loadCapacitorPushNotificationsPlugin(); + + if (!LocalNotifications) { + return; + } + + await LocalNotifications.createChannel({ + id: INCOMING_CALL_CHANNEL_ID, + name: 'Incoming calls', + importance: 5, + visibility: 1, + sound: 'call.wav' + }); + + await LocalNotifications.createChannel({ + id: ACTIVE_CALL_CHANNEL_ID, + name: 'Active calls', + importance: 4, + visibility: 1 + }); + + await LocalNotifications.registerActionTypes({ + types: [ + { + id: 'INCOMING_CALL_ACTIONS', + actions: [{ id: 'answer', title: 'Answer' }, { id: 'hangup', title: 'Decline' }] + }, + { + id: 'ACTIVE_CALL_ACTIONS', + actions: [{ id: 'mute', title: 'Mute' }, { id: 'hangup', title: 'Hang up' }] + } + ] + }); + + if (!this.listenersRegistered) { + await LocalNotifications.addListener('localNotificationActionPerformed', (event) => { + const callId = event.notification.extra?.callId as string | undefined; + const intent = resolveCallNotificationAction(event.actionId); + + if (!callId || !intent || !this.actionHandler) { + return; + } + + this.actionHandler({ callId, intent }); + }); + + this.listenersRegistered = true; + } + + if (PushNotifications) { + const permission = await PushNotifications.checkPermissions(); + + if (permission.receive === 'prompt') { + await PushNotifications.requestPermissions(); + } + } + } + + async requestPermission(): Promise { + const LocalNotifications = loadCapacitorLocalNotificationsPlugin(); + + if (!LocalNotifications) { + return false; + } + + const permission = await LocalNotifications.checkPermissions(); + + if (permission.display === 'granted') { + return true; + } + + const requested = await LocalNotifications.requestPermissions(); + + return requested.display === 'granted'; + } + + async showCallNotification(payload: CallNotificationPayload): Promise { + const LocalNotifications = loadCapacitorLocalNotificationsPlugin(); + + if (!LocalNotifications) { + return; + } + + const granted = await this.requestPermission(); + + if (!granted) { + return; + } + + await LocalNotifications.schedule({ + notifications: [ + { + id: payload.id, + title: payload.title, + body: payload.body, + channelId: payload.kind === 'incoming' ? INCOMING_CALL_CHANNEL_ID : ACTIVE_CALL_CHANNEL_ID, + ongoing: payload.kind === 'active', + autoCancel: payload.kind === 'incoming', + extra: { + callId: payload.callId, + kind: payload.kind + }, + actionTypeId: payload.kind === 'active' ? 'ACTIVE_CALL_ACTIONS' : 'INCOMING_CALL_ACTIONS' + } + ] + }); + } + + async dismissCallNotification(callId: string, kind: CallNotificationPayload['kind']): Promise { + const LocalNotifications = loadCapacitorLocalNotificationsPlugin(); + + if (!LocalNotifications) { + return; + } + + const notifications = await LocalNotifications.getDeliveredNotifications(); + const matching = notifications.notifications.filter((notification) => { + const extraCallId = notification.extra?.callId as string | undefined; + const extraKind = notification.extra?.kind as CallNotificationPayload['kind'] | undefined; + + return extraCallId === callId && extraKind === kind; + }); + + if (matching.length === 0) { + return; + } + + await LocalNotifications.removeDeliveredNotifications({ + notifications: matching + }); + } + + onActionSelected(handler: (input: { callId: string; intent: CallNotificationActionIntent }) => void): void { + this.actionHandler = handler; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-persistence.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-persistence.adapter.ts new file mode 100644 index 0000000..0741d2b --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-persistence.adapter.ts @@ -0,0 +1,35 @@ +import type { MobilePersistenceAdapter } from '../../contracts/mobile.contracts'; +import { MobileSqliteConnectionService } from '../../services/mobile-sqlite-connection.service'; + +/** + * Capacitor SQLite persistence adapter. + * + * Initializes native SQLite with schema mirrored from Electron TypeORM entities. + * Domain persistence routes through {@link CapacitorDatabaseService} on Capacitor shells. + */ +export class CapacitorMobilePersistenceAdapter implements MobilePersistenceAdapter { + private initialized = false; + + constructor(private readonly connection: MobileSqliteConnectionService) {} + + get isNativeSqlite(): boolean { + return this.connection.isAvailable; + } + + async initialize(): Promise { + if (this.initialized) { + return; + } + + const store = await this.connection.initialize(); + + if (!store?.isAvailable) { + console.warn('[mobile] SQLite plugin unavailable on this Capacitor shell'); + this.initialized = true; + return; + } + + this.initialized = true; + console.info('[mobile] native SQLite persistence initialized'); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-picture-in-picture.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-picture-in-picture.adapter.ts new file mode 100644 index 0000000..5e0c21c --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-mobile-picture-in-picture.adapter.ts @@ -0,0 +1,43 @@ +import type { MobilePictureInPictureAdapter } from '../../contracts/mobile.contracts'; +import { MetoyouMobile } from './metoyou-mobile.plugin'; +import { WebMobilePictureInPictureAdapter } from '../web/web-mobile-picture-in-picture.adapter'; + +/** Capacitor PiP adapter with Document PiP first and native Android PiP fallback. */ +export class CapacitorMobilePictureInPictureAdapter extends WebMobilePictureInPictureAdapter implements MobilePictureInPictureAdapter { + private nativeSupported: boolean | null = null; + + override isSupported(): boolean { + if (super.isSupported()) { + return true; + } + + return this.nativeSupported === true; + } + + override async enter(videoElement: HTMLVideoElement): Promise { + if (super.isSupported()) { + await super.enter(videoElement); + return; + } + + const result = await MetoyouMobile.enterNativePictureInPicture(); + + this.nativeSupported = result.supported; + + if (!result.supported) { + return; + } + + if (videoElement.paused) { + await videoElement.play().catch(() => {}); + } + } + + override async exit(): Promise { + if (document.pictureInPictureElement) { + await super.exit(); + } + + await MetoyouMobile.exitNativePictureInPicture().catch(() => {}); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.spec.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.spec.ts new file mode 100644 index 0000000..4a09e21 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.spec.ts @@ -0,0 +1,75 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi +} from 'vitest'; + +const capacitorState = vi.hoisted(() => ({ + isNativePlatform: true, + isPluginAvailable: true, + platform: 'android' +})); + +vi.mock('@capacitor/core', () => ({ + Capacitor: { + isNativePlatform: () => capacitorState.isNativePlatform, + isPluginAvailable: (name: string) => capacitorState.isPluginAvailable && name.length > 0, + getPlatform: () => capacitorState.platform + } +})); + +vi.mock('@capacitor/app', () => ({ + App: { + addListener: vi.fn(() => Promise.resolve({ remove: vi.fn() })) + } +})); + +vi.mock('@capacitor/local-notifications', () => ({ + LocalNotifications: { + checkPermissions: vi.fn(() => Promise.resolve({ display: 'granted' })) + } +})); + +import { App } from '@capacitor/app'; +import { LocalNotifications } from '@capacitor/local-notifications'; +import { loadCapacitorAppPlugin, loadCapacitorLocalNotificationsPlugin } from './capacitor-plugin-loader'; + +describe('capacitor-plugin-loader', () => { + beforeEach(() => { + vi.stubGlobal('window', {}); + }); + + afterEach(() => { + capacitorState.isNativePlatform = true; + capacitorState.isPluginAvailable = true; + capacitorState.platform = 'android'; + vi.unstubAllGlobals(); + }); + + it('returns registered plugin instances synchronously without wrapping them in a Promise', () => { + const appPlugin = loadCapacitorAppPlugin(); + const notificationsPlugin = loadCapacitorLocalNotificationsPlugin(); + + expect(appPlugin).toBe(App); + expect(notificationsPlugin).toBe(LocalNotifications); + expect(appPlugin).not.toBeInstanceOf(Promise); + expect(notificationsPlugin).not.toBeInstanceOf(Promise); + }); + + it('returns null when the plugin is unavailable on the active native shell', () => { + capacitorState.isPluginAvailable = false; + + expect(loadCapacitorAppPlugin()).toBeNull(); + expect(loadCapacitorLocalNotificationsPlugin()).toBeNull(); + }); + + it('returns null on non-native shells', () => { + capacitorState.isNativePlatform = false; + + expect(loadCapacitorAppPlugin()).toBeNull(); + expect(loadCapacitorLocalNotificationsPlugin()).toBeNull(); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.ts new file mode 100644 index 0000000..87cb297 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-plugin-loader.ts @@ -0,0 +1,44 @@ +import { App } from '@capacitor/app'; +import { Capacitor } from '@capacitor/core'; +import { Device } from '@capacitor/device'; +import { LocalNotifications } from '@capacitor/local-notifications'; +import { PushNotifications } from '@capacitor/push-notifications'; +import { AudioSession } from '@capgo/capacitor-audio-session'; + +function resolveCapacitorPlugin(pluginName: string, plugin: T): T | null { + if (typeof window === 'undefined' || !Capacitor.isNativePlatform()) { + return null; + } + + if (!Capacitor.isPluginAvailable(pluginName)) { + console.warn(`[mobile] Capacitor plugin "${pluginName}" is not implemented on ${Capacitor.getPlatform()}`); + return null; + } + + return plugin; +} + +/** Resolve the Capacitor App plugin on native shells; returns null on web/electron or when unavailable. */ +export function loadCapacitorAppPlugin(): typeof App | null { + return resolveCapacitorPlugin('App', App); +} + +/** Resolve the Capacitor LocalNotifications plugin on native shells. */ +export function loadCapacitorLocalNotificationsPlugin(): typeof LocalNotifications | null { + return resolveCapacitorPlugin('LocalNotifications', LocalNotifications); +} + +/** Resolve the Capacitor PushNotifications plugin on native shells. */ +export function loadCapacitorPushNotificationsPlugin(): typeof PushNotifications | null { + return resolveCapacitorPlugin('PushNotifications', PushNotifications); +} + +/** Resolve the Capacitor Device plugin on native shells. */ +export function loadCapacitorDevicePlugin(): typeof Device | null { + return resolveCapacitorPlugin('Device', Device); +} + +/** Resolve the Capacitor AudioSession plugin on native shells. */ +export function loadCapacitorAudioSessionPlugin(): typeof AudioSession | null { + return resolveCapacitorPlugin('AudioSession', AudioSession); +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-sqlite.store.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-sqlite.store.ts new file mode 100644 index 0000000..4a74fd0 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/capacitor-sqlite.store.ts @@ -0,0 +1,115 @@ +import { + MOBILE_SQLITE_DATABASE_NAME, + MOBILE_SQLITE_SCHEMA_VERSION, + resolveMobileSqliteMigrationStatements +} from '../../logic/mobile-sqlite-schema.rules'; +import { executeMobileSqliteStatements } from '../../logic/mobile-sqlite-execute.rules'; + +const META_SCHEMA_VERSION_KEY = 'mobile_sqlite_schema_version'; + +export interface MobileSqliteStore { + readonly isAvailable: boolean; + initialize(): Promise; + run(statement: string, values?: unknown[]): Promise; + query(statement: string, values?: unknown[]): Promise; +} + +const schemaInitializationFailures = new Set(); + +/** Lazy-loaded @capacitor-community/sqlite connection for native mobile shells. */ +export async function createCapacitorSqliteStore( + databaseName: string = MOBILE_SQLITE_DATABASE_NAME +): Promise { + if (typeof window === 'undefined') { + return null; + } + + try { + const sqliteModule = await import('@capacitor-community/sqlite'); + + type SqliteDbConnection = import('@capacitor-community/sqlite').SQLiteDBConnection; + + const sqliteConnection = new sqliteModule.SQLiteConnection(sqliteModule.CapacitorSQLite); + + let database: SqliteDbConnection | null = null; + + return { + get isAvailable() { + return database !== null && !schemaInitializationFailures.has(databaseName); + }, + + async initialize(): Promise { + if (schemaInitializationFailures.has(databaseName)) { + throw new Error(`Mobile SQLite schema initialization failed for "${databaseName}".`); + } + + await sqliteConnection.checkConnectionsConsistency(); + const connectionState = await sqliteConnection.isConnection(databaseName, false); + + database = connectionState.result + ? await sqliteConnection.retrieveConnection(databaseName, false) + : await sqliteConnection.createConnection( + databaseName, + false, + 'no-encryption', + MOBILE_SQLITE_SCHEMA_VERSION, + false + ); + + await database.open(); + + let storedVersion = 0; + + try { + const versionRows = await database.query(`SELECT value FROM meta WHERE key = '${META_SCHEMA_VERSION_KEY}' LIMIT 1`); + + storedVersion = Number(versionRows.values?.[0]?.value ?? 0); + } catch { + storedVersion = 0; + } + + const statements = resolveMobileSqliteMigrationStatements(storedVersion); + + if (statements.length === 0) { + return; + } + + try { + const activeDatabase = database; + + if (!activeDatabase) { + throw new Error('Mobile SQLite store is not initialized.'); + } + + await executeMobileSqliteStatements( + (statement) => activeDatabase.execute(statement), + statements + ); + } catch (error) { + schemaInitializationFailures.add(databaseName); + throw error; + } + }, + + async run(statement: string, values: unknown[] = []): Promise { + if (!database) { + throw new Error('Mobile SQLite store is not initialized.'); + } + + await database.run(statement, values); + }, + + async query(statement: string, values: unknown[] = []): Promise { + if (!database) { + throw new Error('Mobile SQLite store is not initialized.'); + } + + const result = await database.query(statement, values); + + return (result.values ?? []) as T[]; + } + }; + } catch { + return null; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/capacitor/metoyou-mobile.plugin.ts b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/metoyou-mobile.plugin.ts new file mode 100644 index 0000000..5771424 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/capacitor/metoyou-mobile.plugin.ts @@ -0,0 +1,14 @@ +import { registerPlugin } from '@capacitor/core'; + +export interface MetoyouMobilePlugin { + setSpeakerphoneEnabled(options: { enabled: boolean }): Promise; + startVoiceForegroundService(): Promise; + stopVoiceForegroundService(): Promise; + enterNativePictureInPicture(): Promise<{ supported: boolean }>; + exitNativePictureInPicture(): Promise; + startCallKitSession(options: { callId: string; displayName: string }): Promise<{ supported: boolean }>; + endCallKitSession(options: { callId: string }): Promise; + isRemotePushConfigured(): Promise<{ configured: boolean }>; +} + +export const MetoyouMobile = registerPlugin('MetoyouMobile'); diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-app-lifecycle.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-app-lifecycle.adapter.ts new file mode 100644 index 0000000..20a2058 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-app-lifecycle.adapter.ts @@ -0,0 +1,20 @@ +import type { MobileAppLifecycleAdapter } from '../../contracts/mobile.contracts'; + +/** Visibility API fallback for browser runtimes. */ +export class WebMobileAppLifecycleAdapter implements MobileAppLifecycleAdapter { + private handler: ((isActive: boolean) => void) | null = null; + + async initialize(): Promise { + if (typeof document === 'undefined') { + return; + } + + document.addEventListener('visibilitychange', () => { + this.handler?.(!document.hidden); + }); + } + + onAppStateChange(handler: (isActive: boolean) => void): void { + this.handler = handler; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-callkit.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-callkit.adapter.ts new file mode 100644 index 0000000..b313b21 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-callkit.adapter.ts @@ -0,0 +1,8 @@ +import type { MobileCallKitAdapter } from '../../contracts/mobile.contracts'; + +/** Web shells do not expose CallKit. */ +export class WebMobileCallKitAdapter implements MobileCallKitAdapter { + async startActiveCall(): Promise {} + + async endActiveCall(): Promise {} +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-media.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-media.adapter.ts new file mode 100644 index 0000000..6ec15f3 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-media.adapter.ts @@ -0,0 +1,49 @@ +import type { MobileMediaAdapter } from '../../contracts/mobile.contracts'; + +/** Web fallback for mobile media affordances. */ +export class WebMobileMediaAdapter implements MobileMediaAdapter { + async pickAttachments(): Promise { + return new Promise((resolve) => { + const input = document.createElement('input'); + + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*,video/*,audio/*,.pdf,.txt,.zip,.rar,.7z,.doc,.docx,.xls,.xlsx,.ppt,.pptx'; + input.style.display = 'none'; + + input.addEventListener('change', () => { + const files = input.files ? Array.from(input.files) : []; + + input.remove(); + resolve(files); + }, { once: true }); + + document.body.appendChild(input); + input.click(); + }); + } + + async setSpeakerphoneEnabled(_enabled: boolean): Promise { + return; + } + + async startBackgroundAudioSession(): Promise { + return; + } + + async stopBackgroundAudioSession(): Promise { + return; + } + + isScreenShareSupported(): boolean { + return typeof navigator !== 'undefined' + && !!navigator.mediaDevices?.getDisplayMedia + && !/iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + } + + isPictureInPictureSupported(): boolean { + return typeof document !== 'undefined' + && 'pictureInPictureEnabled' in document + && document.pictureInPictureEnabled === true; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-notifications.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-notifications.adapter.ts new file mode 100644 index 0000000..32901c0 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-notifications.adapter.ts @@ -0,0 +1,60 @@ +import type { CallNotificationActionIntent, CallNotificationPayload } from '../../logic/call-notification.rules'; +import type { MobileNotificationAdapter } from '../../contracts/mobile.contracts'; + +type CallActionHandler = (input: { callId: string; intent: CallNotificationActionIntent }) => void; + +/** Browser Notification API fallback for web and Capacitor dev shells. */ +export class WebMobileNotificationsAdapter implements MobileNotificationAdapter { + private actionHandler: CallActionHandler | null = null; + + async initialize(): Promise { + return; + } + + async requestPermission(): Promise { + if (typeof Notification === 'undefined') { + return false; + } + + if (Notification.permission === 'granted') { + return true; + } + + if (Notification.permission === 'denied') { + return false; + } + + const permission = await Notification.requestPermission(); + + return permission === 'granted'; + } + + async showCallNotification(payload: CallNotificationPayload): Promise { + const granted = await this.requestPermission(); + + if (!granted) { + return; + } + + const notification = new Notification(payload.title, { + body: payload.body, + tag: `toju-call-${payload.callId}-${payload.kind}` + }); + + notification.onclick = () => { + window.focus(); + this.actionHandler?.({ + callId: payload.callId, + intent: payload.kind === 'incoming' ? 'answer' : 'toggle-mute' + }); + }; + } + + async dismissCallNotification(_callId: string, _kind: CallNotificationPayload['kind']): Promise { + return; + } + + onActionSelected(handler: CallActionHandler): void { + this.actionHandler = handler; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-persistence.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-persistence.adapter.ts new file mode 100644 index 0000000..e5a151b --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-persistence.adapter.ts @@ -0,0 +1,10 @@ +import type { MobilePersistenceAdapter } from '../../contracts/mobile.contracts'; + +/** Web persistence marker - IndexedDB remains the active store via DatabaseService. */ +export class WebMobilePersistenceAdapter implements MobilePersistenceAdapter { + readonly isNativeSqlite = false; + + async initialize(): Promise { + return; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-picture-in-picture.adapter.ts b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-picture-in-picture.adapter.ts new file mode 100644 index 0000000..1713425 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/adapters/web/web-mobile-picture-in-picture.adapter.ts @@ -0,0 +1,26 @@ +import type { MobilePictureInPictureAdapter } from '../../contracts/mobile.contracts'; + +/** Document Picture-in-Picture API adapter for supported browsers. */ +export class WebMobilePictureInPictureAdapter implements MobilePictureInPictureAdapter { + isSupported(): boolean { + return typeof document !== 'undefined' + && 'pictureInPictureEnabled' in document + && document.pictureInPictureEnabled === true; + } + + async enter(videoElement: HTMLVideoElement): Promise { + if (!this.isSupported() || document.pictureInPictureElement) { + return; + } + + await videoElement.requestPictureInPicture(); + } + + async exit(): Promise { + if (!document.pictureInPictureElement) { + return; + } + + await document.exitPictureInPicture(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/contracts/mobile.contracts.ts b/toju-app/src/app/infrastructure/mobile/contracts/mobile.contracts.ts new file mode 100644 index 0000000..9900ee2 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/contracts/mobile.contracts.ts @@ -0,0 +1,46 @@ +import type { CallNotificationActionIntent, CallNotificationPayload } from '../logic/call-notification.rules'; +import type { RuntimePlatform } from '../logic/platform-detection.rules'; + +export interface MobileNotificationAdapter { + initialize(): Promise; + requestPermission(): Promise; + showCallNotification(payload: CallNotificationPayload): Promise; + dismissCallNotification(callId: string, kind: CallNotificationPayload['kind']): Promise; + onActionSelected(handler: (input: { callId: string; intent: CallNotificationActionIntent }) => void): void; +} + +export interface MobileMediaAdapter { + pickAttachments(): Promise; + setSpeakerphoneEnabled(enabled: boolean): Promise; + startBackgroundAudioSession(): Promise; + stopBackgroundAudioSession(): Promise; + isScreenShareSupported(): boolean; + isPictureInPictureSupported(): boolean; +} + +export interface MobilePictureInPictureAdapter { + isSupported(): boolean; + enter(videoElement: HTMLVideoElement): Promise; + exit(): Promise; +} + +export interface MobilePersistenceAdapter { + readonly isNativeSqlite: boolean; + initialize(): Promise; +} + +export interface MobileAppLifecycleAdapter { + initialize(): Promise; + onAppStateChange(handler: (isActive: boolean) => void): void; +} + +export interface MobileCallKitAdapter { + startActiveCall(callId: string, displayName: string): Promise; + endActiveCall(callId: string): Promise; +} + +export interface MobilePlatformSnapshot { + runtime: RuntimePlatform; + isNativeMobile: boolean; + isCapacitor: boolean; +} diff --git a/toju-app/src/app/infrastructure/mobile/index.ts b/toju-app/src/app/infrastructure/mobile/index.ts new file mode 100644 index 0000000..a7c106e --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/index.ts @@ -0,0 +1,12 @@ +export * from './logic/platform-detection.rules'; +export * from './logic/call-notification.rules'; +export * from './services/mobile-platform.service'; +export * from './services/mobile-notifications.service'; +export * from './services/mobile-media.service'; +export * from './services/mobile-picture-in-picture.service'; +export * from './services/mobile-persistence.service'; +export * from './services/mobile-call-session.service'; +export * from './services/mobile-app-lifecycle.service'; +export * from './services/mobile-push-registration.service'; +export * from './services/mobile-callkit.service'; +export * from './services/mobile-sqlite-connection.service'; diff --git a/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.spec.ts new file mode 100644 index 0000000..790cb56 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.spec.ts @@ -0,0 +1,46 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { + buildIncomingCallNotification, + buildInCallNotification, + resolveCallNotificationAction +} from './call-notification.rules'; + +describe('call-notification.rules', () => { + it('builds an incoming call notification payload', () => { + expect(buildIncomingCallNotification('Alex', 'call-1')).toMatchObject({ + title: 'Incoming call', + body: 'Alex is calling you', + callId: 'call-1', + kind: 'incoming', + actions: [{ id: 'answer', title: 'Answer' }, { id: 'hangup', title: 'Decline' }] + }); + }); + + it('builds a persistent in-call notification with action ids', () => { + const payload = buildInCallNotification({ + callId: 'call-2', + displayName: 'Team call', + isMuted: false + }); + + expect(payload).toMatchObject({ + title: 'Team call', + body: 'Call in progress', + callId: 'call-2', + kind: 'active', + actions: [{ id: 'mute', title: 'Mute' }, { id: 'hangup', title: 'Hang up' }] + }); + }); + + it('maps mute action to toggle mute intent', () => { + expect(resolveCallNotificationAction('mute')).toBe('toggle-mute'); + expect(resolveCallNotificationAction('hangup')).toBe('hang-up'); + expect(resolveCallNotificationAction('answer')).toBe('answer'); + expect(resolveCallNotificationAction('unknown')).toBeNull(); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.ts new file mode 100644 index 0000000..f95fcce --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/call-notification.rules.ts @@ -0,0 +1,69 @@ +export type CallNotificationKind = 'incoming' | 'active'; + +export type CallNotificationActionId = 'answer' | 'mute' | 'hangup'; + +export type CallNotificationActionIntent = 'answer' | 'toggle-mute' | 'hang-up'; + +export interface CallNotificationPayload { + id: number; + title: string; + body: string; + callId: string; + kind: CallNotificationKind; + actions?: { id: CallNotificationActionId; title: string }[]; +} + +const INCOMING_CALL_NOTIFICATION_BASE_ID = 1000; +const ACTIVE_CALL_NOTIFICATION_BASE_ID = 2000; + +/** Build a local notification payload for an incoming direct call. */ +export function buildIncomingCallNotification(displayName: string, callId: string): CallNotificationPayload { + return { + id: INCOMING_CALL_NOTIFICATION_BASE_ID + hashCallId(callId), + title: 'Incoming call', + body: `${displayName} is calling you`, + callId, + kind: 'incoming', + actions: [{ id: 'answer', title: 'Answer' }, { id: 'hangup', title: 'Decline' }] + }; +} + +/** Build a persistent in-call notification payload with quick actions. */ +export function buildInCallNotification(input: { + callId: string; + displayName: string; + isMuted: boolean; +}): CallNotificationPayload { + return { + id: ACTIVE_CALL_NOTIFICATION_BASE_ID + hashCallId(input.callId), + title: input.displayName, + body: input.isMuted ? 'Call in progress · muted' : 'Call in progress', + callId: input.callId, + kind: 'active', + actions: [{ id: 'mute', title: input.isMuted ? 'Unmute' : 'Mute' }, { id: 'hangup', title: 'Hang up' }] + }; +} + +/** Map a notification action button id to a call-control intent. */ +export function resolveCallNotificationAction(actionId: string): CallNotificationActionIntent | null { + switch (actionId) { + case 'answer': + return 'answer'; + case 'mute': + return 'toggle-mute'; + case 'hangup': + return 'hang-up'; + default: + return null; + } +} + +function hashCallId(callId: string): number { + let hash = 0; + + for (let index = 0; index < callId.length; index += 1) { + hash = (hash * 31 + callId.charCodeAt(index)) % 997; + } + + return hash; +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.spec.ts new file mode 100644 index 0000000..5f1b9bc --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.spec.ts @@ -0,0 +1,64 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { + buildRemotePushSkipMessage, + resolveRemotePushSkipReason, + shouldRegisterForRemotePush, + type RemotePushRegistrationGateInput +} from './mobile-push-registration.rules'; + +describe('mobile-push-registration.rules', () => { + const configuredInput: RemotePushRegistrationGateInput = { + hasPushPlugin: true, + hasDevicePlugin: true, + remotePushConfigured: true + }; + + it('skips registration when remote push is not configured', () => { + expect( + shouldRegisterForRemotePush({ + ...configuredInput, + remotePushConfigured: false + }) + ).toBe(false); + }); + + it('skips registration when the push plugin is unavailable', () => { + expect( + shouldRegisterForRemotePush({ + ...configuredInput, + hasPushPlugin: false + }) + ).toBe(false); + }); + + it('skips registration when the device plugin is unavailable', () => { + expect( + shouldRegisterForRemotePush({ + ...configuredInput, + hasDevicePlugin: false + }) + ).toBe(false); + }); + + it('allows registration when plugins and remote push are available', () => { + expect(shouldRegisterForRemotePush(configuredInput)).toBe(true); + }); + + it('reports why registration was skipped', () => { + expect( + resolveRemotePushSkipReason({ + ...configuredInput, + remotePushConfigured: false + }) + ).toBe('remote-push-not-configured'); + }); + + it('builds a single actionable skip message for missing Firebase setup', () => { + expect(buildRemotePushSkipMessage('remote-push-not-configured')).toContain('google-services.json'); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.ts new file mode 100644 index 0000000..55e66ae --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-registration.rules.ts @@ -0,0 +1,55 @@ +export type RemotePushSkipReason = + | 'missing-push-plugin' + | 'missing-device-plugin' + | 'remote-push-not-configured' + | 'remote-push-disabled'; + +export interface RemotePushRegistrationGateInput { + hasPushPlugin: boolean; + hasDevicePlugin: boolean; + remotePushConfigured: boolean; + remotePushEnabled?: boolean; +} + +/** Whether the client should call `PushNotifications.register()` on this shell. */ +export function shouldRegisterForRemotePush(input: RemotePushRegistrationGateInput): boolean { + return resolveRemotePushSkipReason(input) === null; +} + +/** Resolve the first reason remote push registration should be skipped. */ +export function resolveRemotePushSkipReason( + input: RemotePushRegistrationGateInput +): RemotePushSkipReason | null { + if (!input.hasPushPlugin) { + return 'missing-push-plugin'; + } + + if (!input.hasDevicePlugin) { + return 'missing-device-plugin'; + } + + if (input.remotePushEnabled === false) { + return 'remote-push-disabled'; + } + + if (!input.remotePushConfigured) { + return 'remote-push-not-configured'; + } + + return null; +} + +/** User-facing console message when remote push registration is skipped. */ +export function buildRemotePushSkipMessage(reason: RemotePushSkipReason): string { + switch (reason) { + case 'missing-push-plugin': + return '[mobile] remote push registration skipped: PushNotifications plugin is unavailable on this shell.'; + case 'missing-device-plugin': + return '[mobile] remote push registration skipped: Device plugin is unavailable on this shell.'; + case 'remote-push-disabled': + return '[mobile] remote push registration skipped: disabled by environment configuration.'; + case 'remote-push-not-configured': + return '[mobile] remote push registration skipped: Firebase/APNs is not configured. ' + + 'Add google-services.json (Android) and rebuild to enable push.'; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.spec.ts new file mode 100644 index 0000000..d01abf6 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.spec.ts @@ -0,0 +1,27 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { buildPushDeviceTokenRegistrationPayload, normalizePushPlatform } from './mobile-push-token.rules'; + +describe('mobile-push-token.rules', () => { + it('normalizes capacitor runtime platforms', () => { + expect(normalizePushPlatform('ios')).toBe('ios'); + expect(normalizePushPlatform('android')).toBe('android'); + expect(normalizePushPlatform('web')).toBeNull(); + }); + + it('builds a registration payload for the signaling server', () => { + expect(buildPushDeviceTokenRegistrationPayload({ + userId: 'user-1', + token: 'abc123', + platform: 'android' + })).toEqual({ + userId: 'user-1', + token: 'abc123', + platform: 'android' + }); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.ts new file mode 100644 index 0000000..f3fc307 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-push-token.rules.ts @@ -0,0 +1,31 @@ +export type MobilePushPlatform = 'ios' | 'android'; + +export interface PushDeviceTokenRegistrationInput { + userId: string; + token: string; + platform: MobilePushPlatform; +} + +export interface PushDeviceTokenRegistrationPayload { + userId: string; + token: string; + platform: MobilePushPlatform; +} + +export function normalizePushPlatform(platform: string): MobilePushPlatform | null { + if (platform === 'ios' || platform === 'android') { + return platform; + } + + return null; +} + +export function buildPushDeviceTokenRegistrationPayload( + input: PushDeviceTokenRegistrationInput +): PushDeviceTokenRegistrationPayload { + return { + userId: input.userId, + token: input.token, + platform: input.platform + }; +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.spec.ts new file mode 100644 index 0000000..be4c8cb --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.spec.ts @@ -0,0 +1,37 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { applyMobileSafeAreaDefaults, getSafeAreaInsetCSSValue } from './mobile-safe-area.rules'; + +describe('mobile-safe-area.rules', () => { + it('builds CSS values with Capacitor and env fallbacks', () => { + expect(getSafeAreaInsetCSSValue('top')).toBe('var(--safe-area-inset-top, env(safe-area-inset-top, 0px))'); + expect(getSafeAreaInsetCSSValue('bottom')).toBe('var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))'); + }); + + it('sets default safe-area variables on the document root', () => { + const properties = new Map(); + const root = { + style: { + getPropertyValue: (property: string) => properties.get(property) ?? '', + setProperty: (property: string, value: string) => { + properties.set(property, value); + } + } + } as unknown as HTMLElement; + + applyMobileSafeAreaDefaults(root); + + expect(properties.get('--safe-area-inset-top')).toBe('0px'); + expect(properties.get('--safe-area-inset-right')).toBe('0px'); + expect(properties.get('--safe-area-inset-bottom')).toBe('0px'); + expect(properties.get('--safe-area-inset-left')).toBe('0px'); + }); + + it('ignores null roots instead of throwing', () => { + expect(() => applyMobileSafeAreaDefaults(null)).not.toThrow(); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.ts new file mode 100644 index 0000000..7f1fa08 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-safe-area.rules.ts @@ -0,0 +1,30 @@ +const SAFE_AREA_SIDES = [ + 'top', + 'right', + 'bottom', + 'left' +] as const; + +export type SafeAreaSide = (typeof SAFE_AREA_SIDES)[number]; + +/** CSS value chain for one safe-area inset (Capacitor vars with env() fallback). */ +export function getSafeAreaInsetCSSValue(side: SafeAreaSide): string { + return `var(--safe-area-inset-${side}, env(safe-area-inset-${side}, 0px))`; +} + +/** Apply default safe-area CSS variables when the document root is available. */ +export function applyMobileSafeAreaDefaults(root: HTMLElement | null = typeof document === 'undefined' + ? null + : document.documentElement): void { + if (!root?.style) { + return; + } + + for (const side of SAFE_AREA_SIDES) { + const property = `--safe-area-inset-${side}`; + + if (!root.style.getPropertyValue(property)) { + root.style.setProperty(property, '0px'); + } + } +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.spec.ts new file mode 100644 index 0000000..96a8aa6 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.spec.ts @@ -0,0 +1,17 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { resolveMobileSqliteDatabaseName } from './mobile-sqlite-database-name.rules'; + +describe('mobile-sqlite-database-name.rules', () => { + it('scopes sqlite files per authenticated user', () => { + expect(resolveMobileSqliteDatabaseName('user-123')).toBe('metoyou__user-123'); + }); + + it('uses an anonymous scope before login', () => { + expect(resolveMobileSqliteDatabaseName(null)).toBe('metoyou__anonymous'); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.ts new file mode 100644 index 0000000..b8e6366 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-database-name.rules.ts @@ -0,0 +1,10 @@ +import { MOBILE_SQLITE_DATABASE_NAME } from './mobile-sqlite-schema.rules'; + +const ANONYMOUS_DATABASE_SCOPE = 'anonymous'; + +/** Mirrors IndexedDB per-user database scoping for Capacitor SQLite files. */ +export function resolveMobileSqliteDatabaseName(userId: string | null): string { + const scopedUserId = userId?.trim() || ANONYMOUS_DATABASE_SCOPE; + + return `${MOBILE_SQLITE_DATABASE_NAME}__${encodeURIComponent(scopedUserId)}`; +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-execute.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-execute.rules.ts new file mode 100644 index 0000000..7d306f1 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-execute.rules.ts @@ -0,0 +1,15 @@ +/** Runs SQLite DDL/DML statements one at a time - required by @capacitor-community/sqlite `execute()`. */ +export async function executeMobileSqliteStatements( + execute: (statement: string) => Promise, + statements: readonly string[] +): Promise { + for (const statement of statements) { + const trimmed = statement.trim(); + + if (!trimmed) { + continue; + } + + await execute(trimmed); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.spec.ts new file mode 100644 index 0000000..90c48ff --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.spec.ts @@ -0,0 +1,65 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { + messageToRow, + rowToMessage, + rowToUser, + userToRow +} from './mobile-sqlite-row-mapper.rules'; + +describe('mobile-sqlite-row-mapper.rules', () => { + it('round-trips message fields including JSON metadata', () => { + const message = { + id: 'm1', + roomId: 'r1', + channelId: 'general', + senderId: 'u1', + senderName: 'Alice', + content: 'hello', + timestamp: 100, + editedAt: 101, + isDeleted: false, + replyToId: 'm0', + linkMetadata: [{ url: 'https://example.com', title: 'Example' }], + kind: 'user' as const, + reactions: [] + }; + + const row = messageToRow(message); + const restored = rowToMessage(row, [{ id: 'rx1', messageId: 'm1', oderId: 'u1', userId: 'u1', emoji: '👍', timestamp: 102 }]); + + expect(restored.id).toBe('m1'); + expect(restored.linkMetadata?.[0]?.title).toBe('Example'); + expect(restored.reactions).toHaveLength(1); + }); + + it('maps booleans to integers for SQLite storage', () => { + const user = userToRow({ + id: 'u1', + oderId: 'u1', + username: 'alice', + displayName: 'Alice', + status: 'online', + role: 'member', + joinedAt: 1, + isOnline: true, + isAdmin: false, + isRoomOwner: true, + voiceState: { muted: false, deafened: false, speaking: false } + }); + + expect(user.isOnline).toBe(1); + expect(user.isAdmin).toBe(0); + expect(user.isRoomOwner).toBe(1); + expect(user.voiceState).toContain('muted'); + + const restored = rowToUser(user); + + expect(restored.isOnline).toBe(true); + expect(restored.voiceState).toEqual({ muted: false, deafened: false, speaking: false }); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.ts new file mode 100644 index 0000000..4c83d18 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-row-mapper.rules.ts @@ -0,0 +1,320 @@ +import { + DELETED_MESSAGE_CONTENT, + type BanEntry, + type Message, + type Reaction, + type Room, + type User +} from '../../../shared-kernel'; +import type { ChatAttachmentMeta, CustomEmoji } from '../../../shared-kernel'; + +export interface MessageRow { + id: string; + roomId: string; + ownerUserId?: string | null; + channelId?: string | null; + senderId: string; + senderName: string; + content: string; + timestamp: number; + editedAt?: number | null; + isDeleted: number; + replyToId?: string | null; + linkMetadata?: string | null; + kind?: string | null; + systemEvent?: string | null; +} + +export interface UserRow { + id: string; + oderId?: string | null; + username?: string | null; + displayName?: string | null; + description?: string | null; + profileUpdatedAt?: number | null; + avatarUrl?: string | null; + avatarHash?: string | null; + avatarMime?: string | null; + avatarUpdatedAt?: number | null; + status?: string | null; + role?: string | null; + joinedAt?: number | null; + peerId?: string | null; + isOnline: number; + isAdmin: number; + isRoomOwner: number; + voiceState?: string | null; + screenShareState?: string | null; + homeSignalServerUrl?: string | null; +} + +export interface RoomRow { + id: string; + name: string; + description?: string | null; + topic?: string | null; + hostId: string; + password?: string | null; + hasPassword: number; + isPrivate: number; + createdAt: number; + userCount: number; + maxUsers?: number | null; + icon?: string | null; + iconUpdatedAt?: number | null; + slowModeInterval: number; + sourceId?: string | null; + sourceName?: string | null; + sourceUrl?: string | null; +} + +function encodeJson(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + + return JSON.stringify(value); +} + +function decodeJson(value: string | null | undefined): T | undefined { + if (!value) { + return undefined; + } + + try { + return JSON.parse(value) as T; + } catch { + return undefined; + } +} + +export function messageToRow(message: Message): MessageRow { + return { + id: message.id, + roomId: message.roomId, + channelId: message.channelId ?? null, + senderId: message.senderId, + senderName: message.senderName, + content: message.content, + timestamp: message.timestamp, + editedAt: message.editedAt ?? null, + isDeleted: message.isDeleted ? 1 : 0, + replyToId: message.replyToId ?? null, + linkMetadata: encodeJson(message.linkMetadata), + kind: message.kind ?? null, + systemEvent: message.systemEvent ?? null + }; +} + +export function rowToMessage(row: MessageRow, reactions: Reaction[] = []): Message { + const message: Message = { + id: row.id, + roomId: row.roomId, + channelId: row.channelId ?? undefined, + senderId: row.senderId, + senderName: row.senderName, + content: row.content, + timestamp: row.timestamp, + editedAt: row.editedAt ?? undefined, + isDeleted: row.isDeleted === 1, + replyToId: row.replyToId ?? undefined, + linkMetadata: decodeJson(row.linkMetadata), + kind: (row.kind as Message['kind']) ?? undefined, + systemEvent: (row.systemEvent as Message['systemEvent']) ?? undefined, + reactions + }; + + if (message.content === DELETED_MESSAGE_CONTENT) { + return { ...message, reactions: [] }; + } + + return message; +} + +export function userToRow(user: User): UserRow { + return { + id: user.id, + oderId: user.oderId, + username: user.username, + displayName: user.displayName, + description: user.description ?? null, + profileUpdatedAt: user.profileUpdatedAt ?? null, + avatarUrl: user.avatarUrl ?? null, + avatarHash: user.avatarHash ?? null, + avatarMime: user.avatarMime ?? null, + avatarUpdatedAt: user.avatarUpdatedAt ?? null, + status: user.status, + role: user.role, + joinedAt: user.joinedAt, + peerId: user.peerId ?? null, + isOnline: user.isOnline ? 1 : 0, + isAdmin: user.isAdmin ? 1 : 0, + isRoomOwner: user.isRoomOwner ? 1 : 0, + voiceState: encodeJson(user.voiceState), + screenShareState: encodeJson(user.screenShareState), + homeSignalServerUrl: user.homeSignalServerUrl ?? null + }; +} + +export function rowToUser(row: UserRow): User { + return { + id: row.id, + oderId: row.oderId ?? row.id, + username: row.username ?? '', + displayName: row.displayName ?? '', + description: row.description ?? undefined, + profileUpdatedAt: row.profileUpdatedAt ?? undefined, + avatarUrl: row.avatarUrl ?? undefined, + avatarHash: row.avatarHash ?? undefined, + avatarMime: row.avatarMime ?? undefined, + avatarUpdatedAt: row.avatarUpdatedAt ?? undefined, + status: (row.status as User['status']) ?? 'offline', + role: (row.role as User['role']) ?? 'member', + joinedAt: row.joinedAt ?? 0, + peerId: row.peerId ?? undefined, + isOnline: row.isOnline === 1, + isAdmin: row.isAdmin === 1, + isRoomOwner: row.isRoomOwner === 1, + voiceState: decodeJson(row.voiceState), + screenShareState: decodeJson(row.screenShareState), + homeSignalServerUrl: row.homeSignalServerUrl ?? undefined + }; +} + +export function roomToRow(room: Room): RoomRow { + return { + id: room.id, + name: room.name, + description: room.description ?? null, + topic: room.topic ?? null, + hostId: room.hostId, + password: room.password ?? null, + hasPassword: room.hasPassword ? 1 : 0, + isPrivate: room.isPrivate ? 1 : 0, + createdAt: room.createdAt, + userCount: room.userCount, + maxUsers: room.maxUsers ?? null, + icon: room.icon ?? null, + iconUpdatedAt: room.iconUpdatedAt ?? null, + slowModeInterval: room.slowModeInterval ?? 0, + sourceId: room.sourceId ?? null, + sourceName: room.sourceName ?? null, + sourceUrl: room.sourceUrl ?? null + }; +} + +export function rowToRoom(row: RoomRow): Room { + return { + id: row.id, + name: row.name, + description: row.description ?? undefined, + topic: row.topic ?? undefined, + hostId: row.hostId, + password: row.password ?? undefined, + hasPassword: row.hasPassword === 1, + isPrivate: row.isPrivate === 1, + createdAt: row.createdAt, + userCount: row.userCount, + maxUsers: row.maxUsers ?? undefined, + icon: row.icon ?? undefined, + iconUpdatedAt: row.iconUpdatedAt ?? undefined, + slowModeInterval: row.slowModeInterval, + sourceId: row.sourceId ?? undefined, + sourceName: row.sourceName ?? undefined, + sourceUrl: row.sourceUrl ?? undefined + }; +} + +export function reactionToValues(reaction: Reaction): unknown[] { + return [ + reaction.id, + reaction.messageId, + reaction.oderId, + reaction.userId, + reaction.emoji, + reaction.timestamp + ]; +} + +export function rowToReaction(row: Reaction): Reaction { + return row; +} + +export function banToValues(ban: BanEntry): unknown[] { + return [ + ban.oderId, + ban.roomId, + ban.userId, + ban.bannedBy, + ban.displayName ?? null, + ban.reason ?? null, + ban.expiresAt ?? null, + ban.timestamp + ]; +} + +export function attachmentToValues(attachment: ChatAttachmentMeta): unknown[] { + return [ + attachment.id, + attachment.messageId, + attachment.filename, + attachment.size, + attachment.mime, + attachment.isImage ? 1 : 0, + attachment.uploaderPeerId ?? null, + attachment.filePath ?? null, + attachment.savedPath ?? null + ]; +} + +export function rowToAttachment(row: { + id: string; + messageId: string; + filename: string; + size: number; + mime: string; + isImage: number; + uploaderPeerId?: string | null; + filePath?: string | null; + savedPath?: string | null; +}): ChatAttachmentMeta { + return { + id: row.id, + messageId: row.messageId, + filename: row.filename, + size: row.size, + mime: row.mime, + isImage: row.isImage === 1, + uploaderPeerId: row.uploaderPeerId ?? undefined, + filePath: row.filePath ?? undefined, + savedPath: row.savedPath ?? undefined + }; +} + +export function customEmojiToValues(emoji: CustomEmoji): unknown[] { + return [ + emoji.id, + emoji.name, + emoji.creatorUserId, + emoji.dataUrl, + emoji.hash, + emoji.mime, + emoji.size, + emoji.createdAt, + emoji.updatedAt + ]; +} + +export function rowToCustomEmoji(row: { + id: string; + name: string; + creatorUserId: string; + dataUrl: string; + hash: string; + mime: string; + size: number; + createdAt: number; + updatedAt: number; +}): CustomEmoji { + return row; +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.spec.ts new file mode 100644 index 0000000..e8696d7 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.spec.ts @@ -0,0 +1,43 @@ +import { + describe, + expect, + it, + vi +} from 'vitest'; + +import { executeMobileSqliteStatements } from './mobile-sqlite-execute.rules'; +import { + MOBILE_SQLITE_SCHEMA_VERSION, + buildMobileSqliteSchemaStatements, + resolveMobileSqliteMigrationStatements +} from './mobile-sqlite-schema.rules'; + +describe('mobile-sqlite-schema.rules', () => { + it('returns one statement per DDL operation for a fresh database', () => { + const statements = resolveMobileSqliteMigrationStatements(0); + + expect(statements.length).toBe(buildMobileSqliteSchemaStatements().length); + expect(statements.length).toBeGreaterThan(1); + + for (const statement of statements) { + expect(statement.trim().length).toBeGreaterThan(0); + expect(statement).not.toMatch(/;\s*CREATE/i); + } + }); + + it('returns no statements when the stored schema version is current', () => { + expect(resolveMobileSqliteMigrationStatements(MOBILE_SQLITE_SCHEMA_VERSION)).toEqual([]); + }); + + it('executes each migration statement separately', async () => { + const execute = vi.fn(() => Promise.resolve()); + const statements = resolveMobileSqliteMigrationStatements(0).slice(0, 3); + + await executeMobileSqliteStatements(execute, statements); + + expect(execute).toHaveBeenCalledTimes(3); + expect(execute.mock.calls.map(([statement]) => statement)).toEqual(statements); + expect(execute.mock.calls[0]?.[0]).toMatch(/^CREATE TABLE IF NOT EXISTS messages/); + expect(execute.mock.calls[1]?.[0]).toMatch(/^CREATE INDEX IF NOT EXISTS idx_messages_room_id/); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.ts new file mode 100644 index 0000000..d300c35 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/mobile-sqlite-schema.rules.ts @@ -0,0 +1,157 @@ +/** Native SQLite database name for Capacitor mobile shells. */ +export const MOBILE_SQLITE_DATABASE_NAME = 'metoyou'; + +/** Bump when adding DDL statements; stored in meta table. */ +export const MOBILE_SQLITE_SCHEMA_VERSION = 2; + +const META_SCHEMA_VERSION_KEY = 'mobile_sqlite_schema_version'; + +/** DDL mirrored from Electron TypeORM entities under `electron/entities/`. */ +export function buildMobileSqliteSchemaStatements(): string[] { + return [ + `CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY NOT NULL, + roomId TEXT NOT NULL, + ownerUserId TEXT, + channelId TEXT, + senderId TEXT NOT NULL, + senderName TEXT NOT NULL, + content TEXT NOT NULL, + timestamp INTEGER NOT NULL, + editedAt INTEGER, + isDeleted INTEGER NOT NULL DEFAULT 0, + replyToId TEXT, + linkMetadata TEXT, + kind TEXT, + systemEvent TEXT + )`, + 'CREATE INDEX IF NOT EXISTS idx_messages_room_id ON messages(roomId)', + 'CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)', + `CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY NOT NULL, + oderId TEXT, + username TEXT, + displayName TEXT, + description TEXT, + profileUpdatedAt INTEGER, + avatarUrl TEXT, + avatarHash TEXT, + avatarMime TEXT, + avatarUpdatedAt INTEGER, + status TEXT, + role TEXT, + joinedAt INTEGER, + peerId TEXT, + isOnline INTEGER NOT NULL DEFAULT 0, + isAdmin INTEGER NOT NULL DEFAULT 0, + isRoomOwner INTEGER NOT NULL DEFAULT 0, + voiceState TEXT, + screenShareState TEXT, + homeSignalServerUrl TEXT + )`, + `CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + topic TEXT, + hostId TEXT NOT NULL, + password TEXT, + hasPassword INTEGER NOT NULL DEFAULT 0, + isPrivate INTEGER NOT NULL DEFAULT 0, + createdAt INTEGER NOT NULL, + userCount INTEGER NOT NULL DEFAULT 0, + maxUsers INTEGER, + icon TEXT, + iconUpdatedAt INTEGER, + slowModeInterval INTEGER NOT NULL DEFAULT 0, + sourceId TEXT, + sourceName TEXT, + sourceUrl TEXT + )`, + 'CREATE INDEX IF NOT EXISTS idx_rooms_created_at ON rooms(createdAt)', + `CREATE TABLE IF NOT EXISTS reactions ( + id TEXT PRIMARY KEY NOT NULL, + messageId TEXT NOT NULL, + oderId TEXT, + userId TEXT, + emoji TEXT NOT NULL, + timestamp INTEGER NOT NULL + )`, + 'CREATE INDEX IF NOT EXISTS idx_reactions_message_id ON reactions(messageId)', + `CREATE TABLE IF NOT EXISTS bans ( + oderId TEXT NOT NULL, + roomId TEXT NOT NULL, + userId TEXT, + bannedBy TEXT NOT NULL, + displayName TEXT, + reason TEXT, + expiresAt INTEGER, + timestamp INTEGER NOT NULL, + PRIMARY KEY (oderId, roomId) + )`, + 'CREATE INDEX IF NOT EXISTS idx_bans_room_id ON bans(roomId)', + `CREATE TABLE IF NOT EXISTS attachments ( + id TEXT PRIMARY KEY NOT NULL, + messageId TEXT NOT NULL, + filename TEXT NOT NULL, + size INTEGER NOT NULL, + mime TEXT NOT NULL, + isImage INTEGER NOT NULL DEFAULT 0, + uploaderPeerId TEXT, + filePath TEXT, + savedPath TEXT + )`, + 'CREATE INDEX IF NOT EXISTS idx_attachments_message_id ON attachments(messageId)', + `CREATE TABLE IF NOT EXISTS custom_emojis ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + creatorUserId TEXT NOT NULL, + dataUrl TEXT NOT NULL, + hash TEXT NOT NULL, + mime TEXT NOT NULL, + size INTEGER NOT NULL, + createdAt INTEGER NOT NULL, + updatedAt INTEGER NOT NULL + )`, + 'CREATE INDEX IF NOT EXISTS idx_custom_emojis_updated_at ON custom_emojis(updatedAt)', + `CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT + )`, + `CREATE TABLE IF NOT EXISTS push_device_tokens ( + id TEXT PRIMARY KEY NOT NULL, + userId TEXT NOT NULL, + platform TEXT NOT NULL, + token TEXT NOT NULL, + updatedAt INTEGER NOT NULL + )`, + 'CREATE INDEX IF NOT EXISTS idx_push_device_tokens_user_id ON push_device_tokens(userId)', + `INSERT OR REPLACE INTO meta (key, value) VALUES ('${META_SCHEMA_VERSION_KEY}', '${MOBILE_SQLITE_SCHEMA_VERSION}')` + ]; +} + +const SCHEMA_V2_MESSAGE_COLUMNS = [ + 'ALTER TABLE messages ADD COLUMN linkMetadata TEXT', + 'ALTER TABLE messages ADD COLUMN kind TEXT', + 'ALTER TABLE messages ADD COLUMN systemEvent TEXT' +]; + +/** Returns DDL statements that still need to run for the stored schema version. */ +export function resolveMobileSqliteMigrationStatements(storedVersion: number): string[] { + if (storedVersion >= MOBILE_SQLITE_SCHEMA_VERSION) { + return []; + } + + if (storedVersion <= 0) { + return buildMobileSqliteSchemaStatements(); + } + + const statements: string[] = []; + + if (storedVersion < 2) { + statements.push(...SCHEMA_V2_MESSAGE_COLUMNS); + statements.push(`INSERT OR REPLACE INTO meta (key, value) VALUES ('${META_SCHEMA_VERSION_KEY}', '2')`); + } + + return statements; +} diff --git a/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.spec.ts b/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.spec.ts new file mode 100644 index 0000000..d40895d --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.spec.ts @@ -0,0 +1,61 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { + detectRuntimePlatform, + isCapacitorNativeRuntime, + shouldUseMobileAttachmentPicker, + type RuntimePlatform +} from './platform-detection.rules'; + +describe('platform-detection.rules', () => { + describe('detectRuntimePlatform', () => { + it('prefers electron when the preload API is present', () => { + expect( + detectRuntimePlatform({ + hasElectronApi: true, + capacitorIsNative: true + }) + ).toBe('electron'); + }); + + it('detects capacitor when running in a native shell without electron', () => { + expect( + detectRuntimePlatform({ + hasElectronApi: false, + capacitorIsNative: true + }) + ).toBe('capacitor'); + }); + + it('falls back to browser for web runtimes', () => { + expect( + detectRuntimePlatform({ + hasElectronApi: false, + capacitorIsNative: false + }) + ).toBe('browser'); + }); + }); + + describe('shouldUseMobileAttachmentPicker', () => { + it('enables the picker on capacitor native and mobile web viewports', () => { + expect(shouldUseMobileAttachmentPicker('capacitor', true)).toBe(true); + expect(shouldUseMobileAttachmentPicker('browser', true)).toBe(true); + }); + + it('keeps drag-and-drop on desktop browser and electron', () => { + expect(shouldUseMobileAttachmentPicker('browser', false)).toBe(false); + expect(shouldUseMobileAttachmentPicker('electron', true)).toBe(false); + }); + }); + + describe('isCapacitorNativeRuntime', () => { + it('returns false when Capacitor is unavailable', () => { + expect(isCapacitorNativeRuntime()).toBe(false); + }); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.ts b/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.ts new file mode 100644 index 0000000..f1b5f3e --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/logic/platform-detection.rules.ts @@ -0,0 +1,45 @@ +export type RuntimePlatform = 'electron' | 'capacitor' | 'browser'; + +export interface PlatformDetectionInput { + hasElectronApi: boolean; + capacitorIsNative?: boolean; +} + +type CapacitorWindow = Window & { + Capacitor?: { + isNativePlatform?: () => boolean; + }; +}; + +/** Resolve the active runtime shell used by the product client. */ +export function detectRuntimePlatform(input: PlatformDetectionInput): RuntimePlatform { + if (input.hasElectronApi) { + return 'electron'; + } + + if (input.capacitorIsNative) { + return 'capacitor'; + } + + return 'browser'; +} + +/** Best-effort detection of a Capacitor native shell without importing Capacitor modules. */ +export function isCapacitorNativeRuntime(): boolean { + if (typeof window === 'undefined') { + return false; + } + + const capacitor = (window as CapacitorWindow).Capacitor; + + return capacitor?.isNativePlatform?.() === true; +} + +/** Whether the chat composer should expose a tap-to-attach control instead of drag-and-drop only. */ +export function shouldUseMobileAttachmentPicker(runtime: RuntimePlatform, isMobileViewport: boolean): boolean { + if (runtime === 'electron') { + return false; + } + + return runtime === 'capacitor' || isMobileViewport; +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-app-lifecycle.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-app-lifecycle.service.ts new file mode 100644 index 0000000..103bf5a --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-app-lifecycle.service.ts @@ -0,0 +1,34 @@ +import { Injectable, inject } from '@angular/core'; + +import type { MobileAppLifecycleAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobileAppLifecycleAdapter } from '../adapters/capacitor/capacitor-mobile-app-lifecycle.adapter'; +import { WebMobileAppLifecycleAdapter } from '../adapters/web/web-mobile-app-lifecycle.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; + +/** Facade for foreground/background lifecycle events. */ +@Injectable({ providedIn: 'root' }) +export class MobileAppLifecycleService { + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly adapter: MobileAppLifecycleAdapter = this.createAdapter(); + private initialized = false; + + async initialize(): Promise { + if (this.initialized) { + return; + } + + await this.adapter.initialize(); + this.mobilePlatform.refreshRuntimeDetection(); + this.initialized = true; + } + + onAppStateChange(handler: (isActive: boolean) => void): void { + this.adapter.onAppStateChange(handler); + } + + private createAdapter(): MobileAppLifecycleAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobileAppLifecycleAdapter() + : new WebMobileAppLifecycleAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-call-session.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-call-session.service.ts new file mode 100644 index 0000000..55e9f5b --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-call-session.service.ts @@ -0,0 +1,135 @@ +import { + DestroyRef, + Injectable, + inject +} from '@angular/core'; +import { Router } from '@angular/router'; + +import type { CallNotificationActionIntent } from '../logic/call-notification.rules'; +import { MobileAppLifecycleService } from './mobile-app-lifecycle.service'; +import { MobileCallKitService } from './mobile-callkit.service'; +import { MobileMediaService } from './mobile-media.service'; +import { MobileNotificationsService } from './mobile-notifications.service'; +import { MobilePictureInPictureService } from './mobile-picture-in-picture.service'; +import { MobilePlatformService } from './mobile-platform.service'; + +export interface ActiveCallSessionState { + callId: string; + displayName: string; + isMuted: boolean; + focusedStreamVideo?: HTMLVideoElement | null; +} + +/** Coordinates in-call notifications, background audio, and stream pop-out for mobile shells. */ +@Injectable({ providedIn: 'root' }) +export class MobileCallSessionService { + private readonly destroyRef = inject(DestroyRef); + private readonly router = inject(Router); + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly notifications = inject(MobileNotificationsService); + private readonly media = inject(MobileMediaService); + private readonly pictureInPicture = inject(MobilePictureInPictureService); + private readonly lifecycle = inject(MobileAppLifecycleService); + private readonly callKit = inject(MobileCallKitService); + + private activeSession: ActiveCallSessionState | null = null; + private actionHandler: ((intent: CallNotificationActionIntent, callId: string) => void) | null = null; + private wired = false; + + initialize(): void { + if (this.wired) { + return; + } + + this.wired = true; + + void this.notifications.initialize(); + void this.lifecycle.initialize(); + + this.notifications.onCallAction(({ callId, intent }) => { + void this.router.navigate(['/call', callId]); + this.actionHandler?.(intent, callId); + }); + + this.lifecycle.onAppStateChange((isActive) => { + void this.handleAppStateChange(isActive); + }); + + this.destroyRef.onDestroy(() => { + this.activeSession = null; + this.actionHandler = null; + }); + } + + onCallControlAction(handler: (intent: CallNotificationActionIntent, callId: string) => void): void { + this.actionHandler = handler; + } + + async notifyIncomingCall(displayName: string, callId: string): Promise { + if (!this.shouldHandleMobileCalls()) { + return; + } + + await this.notifications.showIncomingCall(displayName, callId); + } + + async startActiveCall(session: ActiveCallSessionState): Promise { + if (!this.shouldHandleMobileCalls()) { + return; + } + + this.activeSession = session; + await this.media.startBackgroundAudioSession(); + await this.callKit.startActiveCall(session.callId, session.displayName); + await this.notifications.dismissIncomingCall(session.callId); + await this.notifications.showActiveCall(session); + } + + async updateActiveCall(session: ActiveCallSessionState): Promise { + if (!this.shouldHandleMobileCalls()) { + return; + } + + this.activeSession = session; + await this.notifications.showActiveCall(session); + } + + async endActiveCall(callId: string): Promise { + await this.notifications.dismissIncomingCall(callId); + await this.notifications.dismissActiveCall(callId); + await this.callKit.endActiveCall(callId); + await this.media.stopBackgroundAudioSession(); + await this.pictureInPicture.exit(); + + if (this.activeSession?.callId === callId) { + this.activeSession = null; + } + } + + setFocusedStreamVideo(videoElement: HTMLVideoElement | null): void { + if (!this.activeSession) { + return; + } + + this.activeSession = { + ...this.activeSession, + focusedStreamVideo: videoElement + }; + } + + private shouldHandleMobileCalls(): boolean { + return this.mobilePlatform.isNativeMobile(); + } + + private async handleAppStateChange(isActive: boolean): Promise { + if (!this.activeSession || isActive) { + return; + } + + const video = this.activeSession.focusedStreamVideo; + + if (video && this.pictureInPicture.isSupported()) { + await this.pictureInPicture.enter(video); + } + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-callkit.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-callkit.service.ts new file mode 100644 index 0000000..767c23b --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-callkit.service.ts @@ -0,0 +1,35 @@ +import { Injectable, inject } from '@angular/core'; + +import type { MobileCallKitAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobileCallKitAdapter } from '../adapters/capacitor/capacitor-mobile-callkit.adapter'; +import { WebMobileCallKitAdapter } from '../adapters/web/web-mobile-callkit.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; + +/** Facade for iOS CallKit active-call reporting. */ +@Injectable({ providedIn: 'root' }) +export class MobileCallKitService { + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly adapter: MobileCallKitAdapter = this.createAdapter(); + + startActiveCall(callId: string, displayName: string): Promise { + if (!this.mobilePlatform.isCapacitor()) { + return Promise.resolve(); + } + + return this.adapter.startActiveCall(callId, displayName); + } + + endActiveCall(callId: string): Promise { + if (!this.mobilePlatform.isCapacitor()) { + return Promise.resolve(); + } + + return this.adapter.endActiveCall(callId); + } + + private createAdapter(): MobileCallKitAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobileCallKitAdapter() + : new WebMobileCallKitAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-media.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-media.service.ts new file mode 100644 index 0000000..6d5c140 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-media.service.ts @@ -0,0 +1,42 @@ +import { + Injectable, + computed, + inject +} from '@angular/core'; + +import type { MobileMediaAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobileMediaAdapter } from '../adapters/capacitor/capacitor-mobile-media.adapter'; +import { WebMobileMediaAdapter } from '../adapters/web/web-mobile-media.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; + +/** Facade for mobile media affordances: attachments, speakerphone, background audio, capture limits. */ +@Injectable({ providedIn: 'root' }) +export class MobileMediaService { + readonly isScreenShareSupported = computed(() => this.adapter.isScreenShareSupported()); + readonly isPictureInPictureSupported = computed(() => this.adapter.isPictureInPictureSupported()); + + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly adapter: MobileMediaAdapter = this.createAdapter(); + + pickAttachments(): Promise { + return this.adapter.pickAttachments(); + } + + setSpeakerphoneEnabled(enabled: boolean): Promise { + return this.adapter.setSpeakerphoneEnabled(enabled); + } + + startBackgroundAudioSession(): Promise { + return this.adapter.startBackgroundAudioSession(); + } + + stopBackgroundAudioSession(): Promise { + return this.adapter.stopBackgroundAudioSession(); + } + + private createAdapter(): MobileMediaAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobileMediaAdapter() + : new WebMobileMediaAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-notifications.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-notifications.service.ts new file mode 100644 index 0000000..821ebce --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-notifications.service.ts @@ -0,0 +1,56 @@ +import { Injectable, inject } from '@angular/core'; + +import type { CallNotificationActionIntent } from '../logic/call-notification.rules'; +import { buildIncomingCallNotification, buildInCallNotification } from '../logic/call-notification.rules'; +import type { MobileNotificationAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobileNotificationsAdapter } from '../adapters/capacitor/capacitor-mobile-notifications.adapter'; +import { WebMobileNotificationsAdapter } from '../adapters/web/web-mobile-notifications.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; +import { MobilePushRegistrationService } from './mobile-push-registration.service'; + +/** Facade for push/local notifications with platform-specific adapters. */ +@Injectable({ providedIn: 'root' }) +export class MobileNotificationsService { + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly pushRegistration = inject(MobilePushRegistrationService); + private readonly adapter: MobileNotificationAdapter = this.createAdapter(); + private initialized = false; + + async initialize(): Promise { + if (this.initialized) { + return; + } + + await this.adapter.initialize(); + this.pushRegistration.initialize(); + this.initialized = true; + } + + async showIncomingCall(displayName: string, callId: string): Promise { + await this.initialize(); + await this.adapter.showCallNotification(buildIncomingCallNotification(displayName, callId)); + } + + async showActiveCall(input: { callId: string; displayName: string; isMuted: boolean }): Promise { + await this.initialize(); + await this.adapter.showCallNotification(buildInCallNotification(input)); + } + + async dismissIncomingCall(callId: string): Promise { + await this.adapter.dismissCallNotification(callId, 'incoming'); + } + + async dismissActiveCall(callId: string): Promise { + await this.adapter.dismissCallNotification(callId, 'active'); + } + + onCallAction(handler: (input: { callId: string; intent: CallNotificationActionIntent }) => void): void { + this.adapter.onActionSelected(handler); + } + + private createAdapter(): MobileNotificationAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobileNotificationsAdapter() + : new WebMobileNotificationsAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-persistence.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-persistence.service.ts new file mode 100644 index 0000000..8cfadc9 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-persistence.service.ts @@ -0,0 +1,29 @@ +import { Injectable, inject } from '@angular/core'; + +import type { MobilePersistenceAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobilePersistenceAdapter } from '../adapters/capacitor/capacitor-mobile-persistence.adapter'; +import { WebMobilePersistenceAdapter } from '../adapters/web/web-mobile-persistence.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; +import { MobileSqliteConnectionService } from './mobile-sqlite-connection.service'; + +/** Facade for native SQLite persistence on mobile shells. */ +@Injectable({ providedIn: 'root' }) +export class MobilePersistenceService { + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly sqliteConnection = inject(MobileSqliteConnectionService); + private readonly adapter: MobilePersistenceAdapter = this.createAdapter(); + + get isNativeSqlite(): boolean { + return this.adapter.isNativeSqlite; + } + + initialize(): Promise { + return this.adapter.initialize(); + } + + private createAdapter(): MobilePersistenceAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobilePersistenceAdapter(this.sqliteConnection) + : new WebMobilePersistenceAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-picture-in-picture.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-picture-in-picture.service.ts new file mode 100644 index 0000000..75f0c87 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-picture-in-picture.service.ts @@ -0,0 +1,31 @@ +import { Injectable, inject } from '@angular/core'; + +import type { MobilePictureInPictureAdapter } from '../contracts/mobile.contracts'; +import { CapacitorMobilePictureInPictureAdapter } from '../adapters/capacitor/capacitor-mobile-picture-in-picture.adapter'; +import { WebMobilePictureInPictureAdapter } from '../adapters/web/web-mobile-picture-in-picture.adapter'; +import { MobilePlatformService } from './mobile-platform.service'; + +/** Facade for stream pop-out while the app is backgrounded. */ +@Injectable({ providedIn: 'root' }) +export class MobilePictureInPictureService { + private readonly mobilePlatform = inject(MobilePlatformService); + private readonly adapter: MobilePictureInPictureAdapter = this.createAdapter(); + + isSupported(): boolean { + return this.adapter.isSupported(); + } + + enter(videoElement: HTMLVideoElement): Promise { + return this.adapter.enter(videoElement); + } + + exit(): Promise { + return this.adapter.exit(); + } + + private createAdapter(): MobilePictureInPictureAdapter { + return this.mobilePlatform.isCapacitor() + ? new CapacitorMobilePictureInPictureAdapter() + : new WebMobilePictureInPictureAdapter(); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.spec.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.spec.ts new file mode 100644 index 0000000..7d8213c --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.spec.ts @@ -0,0 +1,46 @@ +import '@angular/compiler'; +import { Injector, runInInjectionContext } from '@angular/core'; +import { + describe, + expect, + it +} from 'vitest'; + +import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; +import { ViewportService } from '../../../core/platform/viewport.service'; +import { MobilePlatformService } from './mobile-platform.service'; + +function createService(options: { isMobile: boolean }): MobilePlatformService { + const injector = Injector.create({ + providers: [ + MobilePlatformService, + { + provide: ElectronBridgeService, + useValue: { isAvailable: false } + }, + { + provide: ViewportService, + useValue: { + isMobile: () => options.isMobile + } + } + ] + }); + + return runInInjectionContext(injector, () => injector.get(MobilePlatformService)); +} + +describe('MobilePlatformService', () => { + it('reports browser runtime and hides attachment button on desktop viewport', () => { + const service = createService({ isMobile: false }); + + expect(service.runtime()).toBe('browser'); + expect(service.shouldShowAttachmentButton()).toBe(false); + }); + + it('enables attachment button on mobile viewport in browser runtime', () => { + const service = createService({ isMobile: true }); + + expect(service.shouldShowAttachmentButton()).toBe(true); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.ts new file mode 100644 index 0000000..9940741 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-platform.service.ts @@ -0,0 +1,51 @@ +import { + Injectable, + computed, + inject, + signal +} from '@angular/core'; + +import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; +import { ViewportService } from '../../../core/platform/viewport.service'; +import { + detectRuntimePlatform, + isCapacitorNativeRuntime, + shouldUseMobileAttachmentPicker, + type RuntimePlatform +} from '../logic/platform-detection.rules'; +import type { MobilePlatformSnapshot } from '../contracts/mobile.contracts'; + +/** Detects runtime shell and exposes mobile capability flags to Angular domains. */ +@Injectable({ providedIn: 'root' }) +export class MobilePlatformService { + readonly runtime = computed(() => this.runtimeSignal()); + readonly isCapacitor = computed(() => this.runtimeSignal() === 'capacitor'); + readonly isNativeMobile = computed(() => this.isCapacitor()); + readonly shouldShowAttachmentButton = computed(() => + shouldUseMobileAttachmentPicker(this.runtimeSignal(), this.viewport.isMobile()) + ); + readonly snapshot = computed(() => ({ + runtime: this.runtimeSignal(), + isNativeMobile: this.isCapacitor(), + isCapacitor: this.isCapacitor() + })); + + private readonly electronBridge = inject(ElectronBridgeService); + private readonly viewport = inject(ViewportService); + private readonly runtimeSignal = signal( + detectRuntimePlatform({ + hasElectronApi: this.electronBridge.isAvailable, + capacitorIsNative: isCapacitorNativeRuntime() + }) + ); + + /** Re-evaluate runtime detection after Capacitor bootstraps on device. */ + refreshRuntimeDetection(): void { + this.runtimeSignal.set( + detectRuntimePlatform({ + hasElectronApi: this.electronBridge.isAvailable, + capacitorIsNative: isCapacitorNativeRuntime() + }) + ); + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.spec.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.spec.ts new file mode 100644 index 0000000..c283b10 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.spec.ts @@ -0,0 +1,111 @@ +import '@angular/compiler'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi +} from 'vitest'; +import { Injector, runInInjectionContext } from '@angular/core'; + +const pushState = vi.hoisted(() => ({ + register: vi.fn(() => Promise.resolve()), + checkPermissions: vi.fn(() => Promise.resolve({ receive: 'granted' })), + requestPermissions: vi.fn(() => Promise.resolve({ receive: 'granted' })), + addListener: vi.fn(() => Promise.resolve({ remove: vi.fn() })) +})); +const deviceState = vi.hoisted(() => ({ + getInfo: vi.fn(() => Promise.resolve({ platform: 'android' })) +})); +const mobilePlatformState = vi.hoisted(() => ({ + isCapacitor: true +})); +const remotePushState = vi.hoisted(() => ({ + configured: true +})); + +vi.mock('../adapters/capacitor/capacitor-plugin-loader', () => ({ + loadCapacitorPushNotificationsPlugin: () => pushState, + loadCapacitorDevicePlugin: () => deviceState +})); + +vi.mock('../adapters/capacitor/metoyou-mobile.plugin', () => ({ + MetoyouMobile: { + isRemotePushConfigured: vi.fn(() => Promise.resolve({ configured: remotePushState.configured })) + } +})); + +import { MetoyouMobile } from '../adapters/capacitor/metoyou-mobile.plugin'; +import { MobilePlatformService } from './mobile-platform.service'; +import { MobilePushRegistrationService } from './mobile-push-registration.service'; + +function createService(): MobilePushRegistrationService { + const injector = Injector.create({ + providers: [ + MobilePushRegistrationService, + { + provide: MobilePlatformService, + useValue: { + isCapacitor: () => mobilePlatformState.isCapacitor + } + } + ] + }); + + return runInInjectionContext(injector, () => injector.get(MobilePushRegistrationService)); +} + +describe('MobilePushRegistrationService', () => { + beforeEach(() => { + vi.spyOn(console, 'warn').mockImplementation(() => {}); + mobilePlatformState.isCapacitor = true; + remotePushState.configured = true; + pushState.register.mockClear(); + pushState.addListener.mockClear(); + vi.mocked(MetoyouMobile.isRemotePushConfigured).mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('skips PushNotifications.register when remote push is unavailable', async () => { + remotePushState.configured = false; + + const service = createService(); + + service.initialize(); + + await vi.waitFor(() => { + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('google-services.json') + ); + }); + + expect(pushState.register).not.toHaveBeenCalled(); + }); + + it('registers for remote push when Firebase/APNs is configured', async () => { + const service = createService(); + + service.initialize(); + + await vi.waitFor(() => { + expect(pushState.register).toHaveBeenCalledTimes(1); + }); + + expect(MetoyouMobile.isRemotePushConfigured).toHaveBeenCalled(); + }); + + it('does not wire listeners on non-capacitor shells', () => { + mobilePlatformState.isCapacitor = false; + + const service = createService(); + + service.initialize(); + + expect(pushState.register).not.toHaveBeenCalled(); + expect(MetoyouMobile.isRemotePushConfigured).not.toHaveBeenCalled(); + }); +}); diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.ts new file mode 100644 index 0000000..c15a2fe --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-push-registration.service.ts @@ -0,0 +1,139 @@ +import { Injectable, inject } from '@angular/core'; + +import { getStoredCurrentUserId } from '../../../core/storage/current-user-storage'; +import { buildPushDeviceTokenRegistrationPayload, normalizePushPlatform } from '../logic/mobile-push-token.rules'; +import { buildRemotePushSkipMessage, resolveRemotePushSkipReason } from '../logic/mobile-push-registration.rules'; +import { loadCapacitorDevicePlugin, loadCapacitorPushNotificationsPlugin } from '../adapters/capacitor/capacitor-plugin-loader'; +import { MetoyouMobile } from '../adapters/capacitor/metoyou-mobile.plugin'; +import { MobilePlatformService } from './mobile-platform.service'; + +/** Registers FCM/APNs device tokens with the signaling server on Capacitor shells. */ +@Injectable({ providedIn: 'root' }) +export class MobilePushRegistrationService { + private readonly mobilePlatform = inject(MobilePlatformService); + private wired = false; + private latestToken: string | null = null; + + initialize(): void { + if (this.wired || !this.mobilePlatform.isCapacitor()) { + return; + } + + this.wired = true; + void this.registerPushListeners(); + } + + async registerCurrentToken(): Promise { + if (!this.latestToken) { + return; + } + + await this.persistToken(this.latestToken); + } + + private async registerPushListeners(): Promise { + const PushNotifications = loadCapacitorPushNotificationsPlugin(); + const Device = loadCapacitorDevicePlugin(); + const remotePushConfigured = await this.isRemotePushConfigured(); + const skipReason = resolveRemotePushSkipReason({ + hasPushPlugin: !!PushNotifications, + hasDevicePlugin: !!Device, + remotePushConfigured + }); + + if (skipReason) { + console.warn(buildRemotePushSkipMessage(skipReason)); + return; + } + + const pushNotifications = PushNotifications; + + if (!pushNotifications) { + return; + } + + try { + const permission = await pushNotifications.checkPermissions(); + + if (permission.receive === 'prompt') { + await pushNotifications.requestPermissions(); + } + + await pushNotifications.addListener('registration', (event) => { + this.latestToken = event.value; + void this.persistToken(event.value); + }); + + await pushNotifications.addListener('registrationError', (error) => { + console.warn('[mobile] push registration failed', error); + }); + + await pushNotifications.register(); + } catch (error) { + console.warn('[mobile] remote push registration skipped after failure', error); + } + } + + private async isRemotePushConfigured(): Promise { + try { + const result = await MetoyouMobile.isRemotePushConfigured(); + + return result.configured === true; + } catch { + return false; + } + } + + private async persistToken(token: string): Promise { + const userId = getStoredCurrentUserId(); + + if (!userId) { + return; + } + + const Device = loadCapacitorDevicePlugin(); + const deviceInfo = Device ? await Device.getInfo() : null; + const platform = normalizePushPlatform(deviceInfo?.platform ?? ''); + + if (!platform) { + return; + } + + const payload = buildPushDeviceTokenRegistrationPayload({ + userId, + token, + platform + }); + const serverUrl = this.resolveSignalingServerUrl(); + + if (!serverUrl) { + return; + } + + try { + await fetch(`${serverUrl}/api/users/device-tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + } catch (error) { + console.warn('[mobile] failed to persist push device token', error); + } + } + + private resolveSignalingServerUrl(): string | null { + if (typeof window === 'undefined') { + return null; + } + + const configured = window.localStorage.getItem('metoyou.signalServerUrl'); + + if (configured) { + return configured.replace(/\/$/, ''); + } + + return `${window.location.origin}`; + } +} diff --git a/toju-app/src/app/infrastructure/mobile/services/mobile-sqlite-connection.service.ts b/toju-app/src/app/infrastructure/mobile/services/mobile-sqlite-connection.service.ts new file mode 100644 index 0000000..bb8c109 --- /dev/null +++ b/toju-app/src/app/infrastructure/mobile/services/mobile-sqlite-connection.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; + +import { createCapacitorSqliteStore, type MobileSqliteStore } from '../adapters/capacitor/capacitor-sqlite.store'; +import { resolveMobileSqliteDatabaseName } from '../logic/mobile-sqlite-database-name.rules'; +import { getStoredCurrentUserId } from '../../../core/storage/current-user-storage'; + +/** Shared native SQLite connection used by mobile persistence and DatabaseService. */ +@Injectable({ providedIn: 'root' }) +export class MobileSqliteConnectionService { + private store: MobileSqliteStore | null = null; + private activeDatabaseName: string | null = null; + private initializationPromise: Promise | null = null; + private initializationFailed = false; + + get isAvailable(): boolean { + return this.store?.isAvailable === true; + } + + async initialize(): Promise { + if (this.initializationFailed) { + return null; + } + + if (this.initializationPromise) { + return this.initializationPromise; + } + + this.initializationPromise = this.openStore() + .catch((error) => { + this.initializationFailed = true; + console.error('[mobile] SQLite initialization failed', error); + return null; + }) + .finally(() => { + this.initializationPromise = null; + }); + + return this.initializationPromise; + } + + async getStore(): Promise { + const store = await this.initialize(); + + if (!store?.isAvailable) { + throw new Error('Native SQLite store is unavailable on this shell.'); + } + + return store; + } + + private async openStore(): Promise { + const databaseName = resolveMobileSqliteDatabaseName(getStoredCurrentUserId()); + + if (this.store && this.activeDatabaseName === databaseName && this.store.isAvailable) { + return this.store; + } + + this.store = await createCapacitorSqliteStore(databaseName); + + if (!this.store) { + return null; + } + + await this.store.initialize(); + this.activeDatabaseName = databaseName; + + return this.store; + } +} diff --git a/toju-app/src/app/infrastructure/persistence/capacitor-database.service.ts b/toju-app/src/app/infrastructure/persistence/capacitor-database.service.ts new file mode 100644 index 0000000..a186bae --- /dev/null +++ b/toju-app/src/app/infrastructure/persistence/capacitor-database.service.ts @@ -0,0 +1,503 @@ +import { Injectable, inject } from '@angular/core'; +import { + DELETED_MESSAGE_CONTENT, + type BanEntry, + type Message, + type Reaction, + type Room, + type User +} from '../../shared-kernel'; +import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel'; +import { getStoredCurrentUserId } from '../../core/storage/current-user-storage'; +import { + attachmentToValues, + banToValues, + customEmojiToValues, + messageToRow, + reactionToValues, + roomToRow, + rowToAttachment, + rowToCustomEmoji, + rowToMessage, + rowToRoom, + rowToUser, + userToRow, + type MessageRow, + type RoomRow, + type UserRow +} from '../mobile/logic/mobile-sqlite-row-mapper.rules'; +import { MobileSqliteConnectionService } from '../mobile/services/mobile-sqlite-connection.service'; +import type { RoomMessageStats } from './database.service'; + +/** + * SQLite-backed database service for Capacitor native shells. + * + * Mirrors the {@link BrowserDatabaseService} API using `@capacitor-community/sqlite`. + */ +@Injectable({ providedIn: 'root' }) +export class CapacitorDatabaseService { + private readonly connection = inject(MobileSqliteConnectionService); + + async initialize(): Promise { + await this.connection.initialize(); + } + + async saveMessage(message: Message): Promise { + const store = await this.connection.getStore(); + const row = messageToRow(message); + + await store.run( + `INSERT OR REPLACE INTO messages ( + id, roomId, ownerUserId, channelId, senderId, senderName, content, + timestamp, editedAt, isDeleted, replyToId, linkMetadata, kind, systemEvent + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + row.id, + row.roomId, + row.ownerUserId ?? null, + row.channelId ?? null, + row.senderId, + row.senderName, + row.content, + row.timestamp, + row.editedAt ?? null, + row.isDeleted, + row.replyToId ?? null, + row.linkMetadata ?? null, + row.kind ?? null, + row.systemEvent ?? null + ] + ); + } + + async getMessages( + roomId: string, + limit = 100, + offset = 0, + channelId?: string, + beforeTimestamp?: number + ): Promise { + const store = await this.connection.getStore(); + const rows = await store.query(` + SELECT * FROM messages + WHERE roomId = ? + ${beforeTimestamp !== undefined ? 'AND timestamp < ?' : ''} + ORDER BY timestamp ASC + `, beforeTimestamp !== undefined ? [roomId, beforeTimestamp] : [roomId]); + const scopedRows = channelId + ? rows.filter((row) => (row.channelId || 'general') === channelId) + : rows; + const endIndex = Math.max(scopedRows.length - offset, 0); + const startIndex = Math.max(endIndex - limit, 0); + const slice = scopedRows.slice(startIndex, endIndex); + + return this.hydrateMessages(slice.map((row) => rowToMessage(row))); + } + + async getMessagesSince(roomId: string, sinceTimestamp: number): Promise { + const store = await this.connection.getStore(); + const rows = await store.query( + 'SELECT * FROM messages WHERE roomId = ? AND timestamp > ? ORDER BY timestamp ASC', + [roomId, sinceTimestamp] + ); + + return this.hydrateMessages(rows.map((row) => rowToMessage(row))); + } + + async getRoomMessageStats(roomId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query<{ count: number; lastUpdated: number }>( + `SELECT COUNT(*) as count, MAX(COALESCE(editedAt, timestamp, 0)) as lastUpdated + FROM messages WHERE roomId = ?`, + [roomId] + ); + + return { + count: Number(rows[0]?.count ?? 0), + lastUpdated: Number(rows[0]?.lastUpdated ?? 0) + }; + } + + async deleteMessage(messageId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM messages WHERE id = ?', [messageId]); + } + + async updateMessage(messageId: string, updates: Partial): Promise { + const existing = await this.getMessageById(messageId); + + if (existing) { + await this.saveMessage({ ...existing, ...updates }); + } + } + + async getMessageById(messageId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query('SELECT * FROM messages WHERE id = ? LIMIT 1', [messageId]); + const row = rows[0]; + + if (!row) { + return null; + } + + const messages = await this.hydrateMessages([rowToMessage(row)]); + + return messages[0] ?? null; + } + + async clearRoomMessages(roomId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM messages WHERE roomId = ?', [roomId]); + } + + async saveReaction(reaction: Reaction): Promise { + const store = await this.connection.getStore(); + const existing = await store.query( + 'SELECT * FROM reactions WHERE messageId = ? AND userId = ? AND emoji = ? LIMIT 1', + [ + reaction.messageId, + reaction.userId, + reaction.emoji + ] + ); + + if (existing.length === 0) { + await store.run( + 'INSERT OR REPLACE INTO reactions (id, messageId, oderId, userId, emoji, timestamp) VALUES (?, ?, ?, ?, ?, ?)', + reactionToValues(reaction) + ); + } + } + + async removeReaction(messageId: string, userId: string, emoji: string): Promise { + const store = await this.connection.getStore(); + + await store.run( + 'DELETE FROM reactions WHERE messageId = ? AND userId = ? AND emoji = ?', + [ + messageId, + userId, + emoji + ] + ); + } + + async getReactionsForMessage(messageId: string): Promise { + const store = await this.connection.getStore(); + + return store.query( + 'SELECT * FROM reactions WHERE messageId = ? ORDER BY timestamp ASC', + [messageId] + ); + } + + async saveUser(user: User): Promise { + const store = await this.connection.getStore(); + const row = userToRow(user); + + await store.run( + `INSERT OR REPLACE INTO users ( + id, oderId, username, displayName, description, profileUpdatedAt, + avatarUrl, avatarHash, avatarMime, avatarUpdatedAt, status, role, + joinedAt, peerId, isOnline, isAdmin, isRoomOwner, voiceState, + screenShareState, homeSignalServerUrl + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + row.id, + row.oderId ?? null, + row.username ?? null, + row.displayName ?? null, + row.description ?? null, + row.profileUpdatedAt ?? null, + row.avatarUrl ?? null, + row.avatarHash ?? null, + row.avatarMime ?? null, + row.avatarUpdatedAt ?? null, + row.status ?? null, + row.role ?? null, + row.joinedAt ?? null, + row.peerId ?? null, + row.isOnline, + row.isAdmin, + row.isRoomOwner, + row.voiceState ?? null, + row.screenShareState ?? null, + row.homeSignalServerUrl ?? null + ] + ); + } + + async getUser(userId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]); + + return rows[0] ? rowToUser(rows[0]) : null; + } + + async getCurrentUser(): Promise { + const userId = await this.getCurrentUserId(); + + return userId ? this.getUser(userId) : null; + } + + async getCurrentUserId(): Promise { + const store = await this.connection.getStore(); + const rows = await store.query<{ value: string }>( + "SELECT value FROM meta WHERE key = 'currentUserId' LIMIT 1" + ); + + return rows[0]?.value?.trim() || null; + } + + async setCurrentUserId(userId: string): Promise { + const store = await this.connection.getStore(); + + await store.run( + "INSERT OR REPLACE INTO meta (key, value) VALUES ('currentUserId', ?)", + [userId] + ); + + if (getStoredCurrentUserId() !== userId) { + await this.connection.initialize(); + } + } + + async getUsersByRoom(_roomId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query('SELECT * FROM users'); + + return rows.map(rowToUser); + } + + async updateUser(userId: string, updates: Partial): Promise { + const existing = await this.getUser(userId); + + if (existing) { + await this.saveUser({ ...existing, ...updates }); + } + } + + async saveRoom(room: Room): Promise { + const store = await this.connection.getStore(); + const row = roomToRow(room); + + await store.run( + `INSERT OR REPLACE INTO rooms ( + id, name, description, topic, hostId, password, hasPassword, isPrivate, + createdAt, userCount, maxUsers, icon, iconUpdatedAt, slowModeInterval, + sourceId, sourceName, sourceUrl + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + row.id, + row.name, + row.description ?? null, + row.topic ?? null, + row.hostId, + row.password ?? null, + row.hasPassword, + row.isPrivate, + row.createdAt, + row.userCount, + row.maxUsers ?? null, + row.icon ?? null, + row.iconUpdatedAt ?? null, + row.slowModeInterval, + row.sourceId ?? null, + row.sourceName ?? null, + row.sourceUrl ?? null + ] + ); + } + + async getRoom(roomId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query('SELECT * FROM rooms WHERE id = ? LIMIT 1', [roomId]); + + return rows[0] ? rowToRoom(rows[0]) : null; + } + + async getAllRooms(): Promise { + const store = await this.connection.getStore(); + const rows = await store.query('SELECT * FROM rooms ORDER BY createdAt ASC'); + + return rows.map(rowToRoom); + } + + async deleteRoom(roomId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM rooms WHERE id = ?', [roomId]); + await this.clearRoomMessages(roomId); + } + + async updateRoom(roomId: string, updates: Partial): Promise { + const existing = await this.getRoom(roomId); + + if (existing) { + await this.saveRoom({ ...existing, ...updates }); + } + } + + async saveBan(ban: BanEntry): Promise { + const store = await this.connection.getStore(); + + await store.run( + `INSERT OR REPLACE INTO bans ( + oderId, roomId, userId, bannedBy, displayName, reason, expiresAt, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + banToValues(ban) + ); + } + + async removeBan(oderId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM bans WHERE oderId = ?', [oderId]); + } + + async getBansForRoom(roomId: string): Promise { + const store = await this.connection.getStore(); + const now = Date.now(); + const rows = await store.query( + 'SELECT * FROM bans WHERE roomId = ?', + [roomId] + ); + + return rows.filter((ban) => !ban.expiresAt || ban.expiresAt > now); + } + + async isUserBanned(userId: string, roomId: string): Promise { + const activeBans = await this.getBansForRoom(roomId); + + return activeBans.some((ban) => ban.oderId === userId); + } + + async saveAttachment(attachment: ChatAttachmentMeta): Promise { + const store = await this.connection.getStore(); + + await store.run( + `INSERT OR REPLACE INTO attachments ( + id, messageId, filename, size, mime, isImage, uploaderPeerId, filePath, savedPath + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + attachmentToValues(attachment) + ); + } + + async getAttachmentsForMessage(messageId: string): Promise { + const store = await this.connection.getStore(); + const rows = await store.query[0]>( + 'SELECT * FROM attachments WHERE messageId = ?', + [messageId] + ); + + return rows.map(rowToAttachment); + } + + async getAllAttachments(): Promise { + const store = await this.connection.getStore(); + const rows = await store.query[0]>('SELECT * FROM attachments'); + + return rows.map(rowToAttachment); + } + + async saveCustomEmoji(emoji: CustomEmoji): Promise { + const store = await this.connection.getStore(); + + await store.run( + `INSERT OR REPLACE INTO custom_emojis ( + id, name, creatorUserId, dataUrl, hash, mime, size, createdAt, updatedAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + customEmojiToValues(emoji) + ); + } + + async getCustomEmojis(): Promise { + const store = await this.connection.getStore(); + const rows = await store.query[0]>('SELECT * FROM custom_emojis'); + + return rows.map(rowToCustomEmoji); + } + + async deleteCustomEmoji(emojiId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM custom_emojis WHERE id = ?', [emojiId]); + } + + async deleteAttachmentsForMessage(messageId: string): Promise { + const store = await this.connection.getStore(); + + await store.run('DELETE FROM attachments WHERE messageId = ?', [messageId]); + } + + async clearAllData(): Promise { + const store = await this.connection.getStore(); + const tables = [ + 'messages', + 'users', + 'rooms', + 'reactions', + 'bans', + 'attachments', + 'custom_emojis', + 'meta', + 'push_device_tokens' + ]; + + for (const table of tables) { + await store.run(`DELETE FROM ${table}`); + } + } + + private async hydrateMessages(messages: Message[]): Promise { + if (messages.length === 0) { + return []; + } + + const reactionsByMessageId = await this.loadReactionsForMessages(messages.map((message) => message.id)); + + return messages.map((message) => this.normaliseMessage({ + ...message, + reactions: reactionsByMessageId.get(message.id) ?? message.reactions ?? [] + })); + } + + private async loadReactionsForMessages(messageIds: readonly string[]): Promise> { + const messageIdSet = new Set(messageIds.filter((messageId) => messageId.trim().length > 0)); + const reactionsByMessageId = new Map(); + + if (messageIdSet.size === 0) { + return reactionsByMessageId; + } + + const store = await this.connection.getStore(); + const allReactions = await store.query('SELECT * FROM reactions'); + + for (const reaction of allReactions) { + if (!messageIdSet.has(reaction.messageId)) { + continue; + } + + const reactions = reactionsByMessageId.get(reaction.messageId) ?? []; + + reactions.push(reaction); + reactionsByMessageId.set(reaction.messageId, reactions); + } + + for (const reactions of reactionsByMessageId.values()) { + reactions.sort((first, second) => first.timestamp - second.timestamp); + } + + return reactionsByMessageId; + } + + private normaliseMessage(message: Message): Message { + if (message.content === DELETED_MESSAGE_CONTENT) { + return { ...message, reactions: [] }; + } + + return message; + } +} diff --git a/toju-app/src/app/infrastructure/persistence/database-backend.rules.spec.ts b/toju-app/src/app/infrastructure/persistence/database-backend.rules.spec.ts new file mode 100644 index 0000000..1dfcfc0 --- /dev/null +++ b/toju-app/src/app/infrastructure/persistence/database-backend.rules.spec.ts @@ -0,0 +1,25 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { resolveDatabaseBackend } from './database-backend.rules'; + +describe('database-backend.rules', () => { + it('routes Electron to the IPC SQLite backend', () => { + expect(resolveDatabaseBackend({ isElectron: true, isCapacitor: false })).toBe('electron'); + }); + + it('routes Capacitor native shells to SQLite instead of IndexedDB', () => { + expect(resolveDatabaseBackend({ isElectron: false, isCapacitor: true })).toBe('capacitor-sqlite'); + }); + + it('routes plain browser shells to IndexedDB', () => { + expect(resolveDatabaseBackend({ isElectron: false, isCapacitor: false })).toBe('browser'); + }); + + it('prefers Electron when both Electron and Capacitor flags are set', () => { + expect(resolveDatabaseBackend({ isElectron: true, isCapacitor: true })).toBe('electron'); + }); +}); diff --git a/toju-app/src/app/infrastructure/persistence/database-backend.rules.ts b/toju-app/src/app/infrastructure/persistence/database-backend.rules.ts new file mode 100644 index 0000000..e6901fe --- /dev/null +++ b/toju-app/src/app/infrastructure/persistence/database-backend.rules.ts @@ -0,0 +1,17 @@ +export type DatabaseBackendKind = 'electron' | 'capacitor-sqlite' | 'browser'; + +/** Selects the persistence backend for the current runtime shell. */ +export function resolveDatabaseBackend(input: { + isElectron: boolean; + isCapacitor: boolean; +}): DatabaseBackendKind { + if (input.isElectron) { + return 'electron'; + } + + if (input.isCapacitor) { + return 'capacitor-sqlite'; + } + + return 'browser'; +} diff --git a/toju-app/src/app/infrastructure/persistence/database.service.spec.ts b/toju-app/src/app/infrastructure/persistence/database.service.spec.ts index 4fc1ba6..6bd41e1 100644 --- a/toju-app/src/app/infrastructure/persistence/database.service.spec.ts +++ b/toju-app/src/app/infrastructure/persistence/database.service.spec.ts @@ -12,6 +12,7 @@ import { import { PlatformService } from '../../core/platform'; import { BrowserDatabaseService } from './browser-database.service'; +import { CapacitorDatabaseService } from './capacitor-database.service'; import { DatabaseService } from './database.service'; import { ElectronDatabaseService } from './electron-database.service'; @@ -20,6 +21,10 @@ describe('DatabaseService', () => { getBansForRoom: ReturnType; initialize: ReturnType; }; + let capacitorDatabase: { + getBansForRoom: ReturnType; + initialize: ReturnType; + }; let electronDatabase: { getBansForRoom: ReturnType; initialize: ReturnType; @@ -30,18 +35,23 @@ describe('DatabaseService', () => { getBansForRoom: vi.fn(() => Promise.resolve([])), initialize: vi.fn(() => Promise.resolve()) }; + capacitorDatabase = { + getBansForRoom: vi.fn(() => Promise.resolve([])), + initialize: vi.fn(() => Promise.resolve()) + }; electronDatabase = { getBansForRoom: vi.fn(() => Promise.resolve([])), initialize: vi.fn(() => Promise.resolve()) }; }); - function createService(): DatabaseService { + function createService(platform: Pick): DatabaseService { const injector = Injector.create({ providers: [ DatabaseService, - { provide: PlatformService, useValue: { isBrowser: true, isElectron: false } }, + { provide: PlatformService, useValue: platform }, { provide: BrowserDatabaseService, useValue: browserDatabase }, + { provide: CapacitorDatabaseService, useValue: capacitorDatabase }, { provide: ElectronDatabaseService, useValue: electronDatabase } ] }); @@ -49,13 +59,35 @@ describe('DatabaseService', () => { return runInInjectionContext(injector, () => injector.get(DatabaseService)); } - it('initializes the selected backend before the first delegated read', async () => { - const service = createService(); + it('initializes the browser backend before the first delegated read', async () => { + const service = createService({ isBrowser: true, isElectron: false, isCapacitor: false }); await service.getBansForRoom('room-1'); expect(browserDatabase.initialize).toHaveBeenCalledTimes(1); expect(browserDatabase.getBansForRoom).toHaveBeenCalledWith('room-1'); + expect(capacitorDatabase.initialize).not.toHaveBeenCalled(); expect(service.isReady()).toBe(true); }); -}); \ No newline at end of file + + it('routes Capacitor shells to native SQLite instead of IndexedDB', async () => { + const service = createService({ isBrowser: false, isElectron: false, isCapacitor: true }); + + await service.getBansForRoom('room-1'); + + expect(capacitorDatabase.initialize).toHaveBeenCalledTimes(1); + expect(capacitorDatabase.getBansForRoom).toHaveBeenCalledWith('room-1'); + expect(browserDatabase.initialize).not.toHaveBeenCalled(); + }); + + it('routes Electron shells to the IPC SQLite backend', async () => { + const service = createService({ isBrowser: false, isElectron: true, isCapacitor: false }); + + await service.getBansForRoom('room-1'); + + expect(electronDatabase.initialize).toHaveBeenCalledTimes(1); + expect(electronDatabase.getBansForRoom).toHaveBeenCalledWith('room-1'); + expect(browserDatabase.initialize).not.toHaveBeenCalled(); + expect(capacitorDatabase.initialize).not.toHaveBeenCalled(); + }); +}); diff --git a/toju-app/src/app/infrastructure/persistence/database.service.ts b/toju-app/src/app/infrastructure/persistence/database.service.ts index 9b9a77e..7978f80 100644 --- a/toju-app/src/app/infrastructure/persistence/database.service.ts +++ b/toju-app/src/app/infrastructure/persistence/database.service.ts @@ -14,6 +14,8 @@ import { import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel'; import { PlatformService } from '../../core/platform'; import { BrowserDatabaseService } from './browser-database.service'; +import { CapacitorDatabaseService } from './capacitor-database.service'; +import { resolveDatabaseBackend } from './database-backend.rules'; import { ElectronDatabaseService } from './electron-database.service'; export interface RoomMessageStats { @@ -26,6 +28,7 @@ export interface RoomMessageStats { * storage backend based on the runtime platform. * * - Electron -> SQLite via {@link ElectronDatabaseService} (IPC to main process). + * - Capacitor -> native SQLite via {@link CapacitorDatabaseService}. * - Browser -> IndexedDB via {@link BrowserDatabaseService}. * * All consumers inject `DatabaseService`; the underlying storage engine @@ -35,6 +38,7 @@ export interface RoomMessageStats { export class DatabaseService { private readonly platform = inject(PlatformService); private readonly browserDb = inject(BrowserDatabaseService); + private readonly capacitorDb = inject(CapacitorDatabaseService); private readonly electronDb = inject(ElectronDatabaseService); private initializationPromise: Promise | null = null; @@ -43,7 +47,20 @@ export class DatabaseService { /** The active storage backend for the current platform. */ private get backend() { - return this.platform.isBrowser ? this.browserDb : this.electronDb; + const backendKind = resolveDatabaseBackend({ + isElectron: this.platform.isElectron, + isCapacitor: this.platform.isCapacitor + }); + + if (backendKind === 'electron') { + return this.electronDb; + } + + if (backendKind === 'capacitor-sqlite') { + return this.capacitorDb; + } + + return this.browserDb; } /** Initialise the platform-specific database. */ diff --git a/toju-app/src/app/shared/components/bottom-sheet/bottom-sheet.component.html b/toju-app/src/app/shared/components/bottom-sheet/bottom-sheet.component.html index 49396b2..3f2126c 100644 --- a/toju-app/src/app/shared/components/bottom-sheet/bottom-sheet.component.html +++ b/toju-app/src/app/shared/components/bottom-sheet/bottom-sheet.component.html @@ -36,7 +36,7 @@ -
+
diff --git a/toju-app/src/index.html b/toju-app/src/index.html index dc54d25..46f8acc 100644 --- a/toju-app/src/index.html +++ b/toju-app/src/index.html @@ -1,12 +1,15 @@ - + MeToYou , ) globally. registerSwiperElements(); +// Ensure Capacitor SystemBars injection has a document root with safe-area defaults. +applyMobileSafeAreaDefaults(); + // Expose mermaid globally for ngx-remark's MermaidComponent window.mermaid = mermaid; mermaid.initialize({ diff --git a/toju-app/src/styles.scss b/toju-app/src/styles.scss index 1fb77b2..c49134b 100644 --- a/toju-app/src/styles.scss +++ b/toju-app/src/styles.scss @@ -124,12 +124,34 @@ @apply border-border; } + html { + box-sizing: border-box; + height: 100%; + padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px)); + padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px)); + padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)); + padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px)); + } + + *, + *::before, + *::after { + box-sizing: inherit; + } + body { @apply bg-background text-foreground; + height: 100%; + margin: 0; font-feature-settings: 'rlig' 1, 'calt' 1; } + + app-root { + display: block; + height: 100%; + } } /* Scrollbar styling */ diff --git a/tools/build-android-apk.sh b/tools/build-android-apk.sh new file mode 100755 index 0000000..0d084b5 --- /dev/null +++ b/tools/build-android-apk.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Build a debug Android APK for the Capacitor shell. +# +# Prerequisites (CI installs these automatically): +# - Node.js 22 + root npm dependencies (`npm ci`) +# - JDK 21 (JAVA_HOME) +# - Android SDK with platform 36 + build-tools (ANDROID_SDK_ROOT / ANDROID_HOME) +# +# Output: +# toju-app/android/app/build/outputs/apk/debug/app-debug.apk + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +npm run bundle:rnnoise +npm run build:prod + +cd toju-app +npx cap sync android + +cd android +chmod +x gradlew +./gradlew assembleDebug --no-daemon --stacktrace diff --git a/tools/cap-open-android.js b/tools/cap-open-android.js new file mode 100644 index 0000000..c493160 --- /dev/null +++ b/tools/cap-open-android.js @@ -0,0 +1,45 @@ +'use strict'; + +const { spawn } = require('child_process'); +const path = require('path'); + +const { resolveAndroidStudioPath } = require('./resolve-android-studio-path'); + +function main() { + const studioPath = resolveAndroidStudioPath(); + + if (!studioPath) { + console.error( + '[error] Unable to locate Android Studio (studio.sh).\n' + + ' Install Android Studio or set CAPACITOR_ANDROID_STUDIO_PATH to studio.sh.' + ); + process.exit(1); + } + + const tojuAppDir = path.resolve(__dirname, '..', 'toju-app'); + const child = spawn('npx', ['cap', 'open', 'android'], { + cwd: tojuAppDir, + env: { + ...process.env, + CAPACITOR_ANDROID_STUDIO_PATH: studioPath + }, + stdio: 'inherit', + shell: process.platform === 'win32' + }); + + child.on('error', (error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); + + child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + + process.exit(code ?? 0); + }); +} + +main(); diff --git a/tools/resolve-android-studio-path.js b/tools/resolve-android-studio-path.js new file mode 100644 index 0000000..cc43b09 --- /dev/null +++ b/tools/resolve-android-studio-path.js @@ -0,0 +1,99 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +/** + * @param {string} filePath + * @returns {boolean} + */ +function isExecutableStudioSh(filePath) { + try { + const stat = fs.statSync(filePath); + + return stat.isFile() && (stat.mode & 0o111) !== 0; + } catch { + return false; + } +} + +/** + * @param {string} homeDir + * @returns {string | null} + */ +function findJetBrainsToolboxStudioSh(homeDir) { + const toolboxRoot = path.join( + homeDir, + '.local/share/JetBrains/Toolbox/apps/AndroidStudio' + ); + + let entries; + + try { + entries = fs.readdirSync(toolboxRoot, { withFileTypes: true }); + } catch { + return null; + } + + const studioPaths = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(toolboxRoot, entry.name, 'bin/studio.sh')) + .filter((candidate) => isExecutableStudioSh(candidate)) + .sort(); + + return studioPaths.at(-1) ?? null; +} + +/** + * @param {{ env?: NodeJS.ProcessEnv; homeDir?: string }} [options] + * @returns {string | null} + */ +function resolveAndroidStudioPath(options = {}) { + const env = options.env ?? process.env; + const homeDir = options.homeDir ?? os.homedir(); + + const fromEnv = String(env.CAPACITOR_ANDROID_STUDIO_PATH ?? '').trim(); + + if (fromEnv && isExecutableStudioSh(fromEnv)) { + return fromEnv; + } + + const candidates = [ + '/usr/local/android-studio/bin/studio.sh', + '/opt/android-studio/bin/studio.sh', + path.join(homeDir, 'android-studio/bin/studio.sh'), + '/var/lib/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh', + path.join( + homeDir, + '.local/share/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh' + ), + '/snap/android-studio/current/bin/studio.sh' + ]; + + for (const candidate of candidates) { + if (isExecutableStudioSh(candidate)) { + return candidate; + } + } + + return findJetBrainsToolboxStudioSh(homeDir); +} + +module.exports = { + resolveAndroidStudioPath, + isExecutableStudioSh +}; + +if (require.main === module) { + const resolved = resolveAndroidStudioPath(); + + if (!resolved) { + console.error( + 'Could not find Android Studio (studio.sh). Install Android Studio or set CAPACITOR_ANDROID_STUDIO_PATH.' + ); + process.exit(1); + } + + process.stdout.write(`${resolved}\n`); +} diff --git a/tools/resolve-android-studio-path.test.js b/tools/resolve-android-studio-path.test.js new file mode 100644 index 0000000..e1f008f --- /dev/null +++ b/tools/resolve-android-studio-path.test.js @@ -0,0 +1,41 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { resolveAndroidStudioPath } = require('./resolve-android-studio-path'); + +test('resolveAndroidStudioPath prefers CAPACITOR_ANDROID_STUDIO_PATH when executable', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'studio-sh-')); + const studioSh = path.join(tempDir, 'studio.sh'); + + fs.writeFileSync(studioSh, '#!/bin/sh\n', { mode: 0o755 }); + + const resolved = resolveAndroidStudioPath({ + env: { CAPACITOR_ANDROID_STUDIO_PATH: studioSh }, + homeDir: tempDir + }); + + assert.equal(resolved, studioSh); + + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +test('resolveAndroidStudioPath finds flatpak active studio.sh when present', () => { + const flatpakPath = + '/var/lib/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh'; + + if (!fs.existsSync(flatpakPath)) { + return; + } + + const resolved = resolveAndroidStudioPath({ + env: {}, + homeDir: '/nonexistent-home-for-test' + }); + + assert.equal(resolved, flatpakPath); +});