From 3356089c13d3ca4089a0a5923588c3de36962778 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:45:06 +0200 Subject: [PATCH] Android app (apps/mobile) with Flutter apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK, with vodozemac (compiled from Rust during the build) for end-to-end encryption. It speaks the plain Client-Server API and uses the web client's design tokens, fonts and symbol. - Sign in by server name (.well-known), homeserver address or full user ID; sign up with the dummy and registration token stages. Sessions survive restarts: SQLite in private storage, credentials also in the Keystore, both excluded from backups and device transfers. - Room list with unread and mention counts, invites, search and filters. Timeline with grouping, day separators, notices, history on scroll, links, images and files (authenticated media). Sending text, photos, camera shots and files, with Sending/Sent/Read/Not sent and Retry. Typing, receipts, deleting. New rooms and DMs, encrypted by default; joining by address; room info with members, invite, rename, topic, notification level, leave. - Encryption: one-step setup behind a recovery key, verifying a new session with the recovery key (restoring the backup) or another session, emoji verification of people and sessions, reset. Undecryptable messages say why. Session list with verify and sign out. - Notifications while running, and push through UnifiedPush (no Firebase): the distributor's endpoint becomes an event_id_only pusher, the gateway is discovered, and a push starts the app headless to fetch, decrypt and show the event. Make targets: mobile (release APKs per ABI into bin/), mobile-run, mobile-test. Kotlin incremental compilation is off (Windows, project and pub cache on different drives), and Tink's JVM artifact is excluded in favour of tink-android. Co-Authored-By: Claude Opus 5.5 --- Makefile | 23 + README.md | 6 +- apps/README.md | 2 +- apps/mobile/.gitignore | 45 + apps/mobile/.metadata | 33 + apps/mobile/README.md | 158 +++ apps/mobile/analysis_options.yaml | 28 + apps/mobile/android/.gitignore | 14 + apps/mobile/android/app/build.gradle.kts | 78 + .../android/app/src/debug/AndroidManifest.xml | 12 + .../android/app/src/main/AndroidManifest.xml | 58 + .../github/dariuszjaniak/warp/MainActivity.kt | 5 + .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 5760 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 3666 bytes .../res/drawable-v21/launch_background.xml | 12 + .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 6802 bytes .../ic_launcher_foreground.png | Bin 0 -> 9860 bytes .../ic_launcher_foreground.png | Bin 0 -> 12733 bytes .../src/main/res/drawable/ic_notification.png | Bin 0 -> 1235 bytes .../main/res/drawable/launch_background.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2642 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1617 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3193 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5065 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6399 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/colors.xml | 4 + .../app/src/main/res/values/styles.xml | 18 + .../main/res/xml/data_extraction_rules.xml | 20 + .../app/src/profile/AndroidManifest.xml | 7 + apps/mobile/android/build.gradle.kts | 24 + apps/mobile/android/gradle.properties | 10 + .../gradle/wrapper/gradle-wrapper.properties | 5 + apps/mobile/android/settings.gradle.kts | 26 + apps/mobile/assets/fonts/Figtree-Variable.ttf | Bin 0 -> 51284 bytes .../assets/fonts/JetBrainsMono-Variable.ttf | Bin 0 -> 105604 bytes apps/mobile/assets/fonts/Outfit-Variable.ttf | Bin 0 -> 95628 bytes apps/mobile/assets/logo/warp-symbol.png | Bin 0 -> 6703 bytes apps/mobile/ios/.gitignore | 34 + .../mobile/ios/Flutter/AppFrameworkInfo.plist | 24 + apps/mobile/ios/Flutter/Debug.xcconfig | 1 + apps/mobile/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 644 +++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + apps/mobile/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 1 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 35861 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 667 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1268 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1863 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 900 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1809 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 2688 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1268 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 2506 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 3690 bytes .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 1521 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 3079 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 1770 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 3456 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 3690 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 5602 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 2312 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 4528 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 2488 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 4904 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 5374 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + apps/mobile/ios/Runner/Info.plist | 70 + .../ios/Runner/Runner-Bridging-Header.h | 1 + apps/mobile/ios/Runner/SceneDelegate.swift | 6 + apps/mobile/ios/RunnerTests/RunnerTests.swift | 12 + apps/mobile/lib/app.dart | 116 ++ apps/mobile/lib/main.dart | 28 + apps/mobile/lib/matrix/client.dart | 183 +++ apps/mobile/lib/matrix/encryption.dart | 95 ++ apps/mobile/lib/matrix/media.dart | 87 ++ apps/mobile/lib/matrix/notifications.dart | 157 +++ apps/mobile/lib/matrix/push.dart | 206 +++ apps/mobile/lib/matrix/timeline.dart | 133 ++ .../mobile/lib/screens/encryption_screen.dart | 374 +++++ apps/mobile/lib/screens/home_screen.dart | 553 ++++++++ apps/mobile/lib/screens/login_screen.dart | 219 +++ apps/mobile/lib/screens/register_screen.dart | 202 +++ apps/mobile/lib/screens/room_info_screen.dart | 425 ++++++ apps/mobile/lib/screens/room_screen.dart | 708 ++++++++++ apps/mobile/lib/screens/sessions_screen.dart | 170 +++ apps/mobile/lib/screens/settings_screen.dart | 413 ++++++ apps/mobile/lib/state.dart | 62 + apps/mobile/lib/theme.dart | 284 ++++ apps/mobile/lib/util/format.dart | 45 + apps/mobile/lib/widgets/avatar.dart | 115 ++ apps/mobile/lib/widgets/message.dart | 694 +++++++++ apps/mobile/lib/widgets/new_chat_sheet.dart | 329 +++++ apps/mobile/lib/widgets/uia.dart | 89 ++ .../lib/widgets/verification_sheet.dart | 234 +++ apps/mobile/pubspec.lock | 1250 +++++++++++++++++ apps/mobile/pubspec.yaml | 93 ++ apps/mobile/test/format_test.dart | 49 + apps/mobile/test/timeline_test.dart | 95 ++ apps/mobile/tool/icons/app-icon.png | Bin 0 -> 34721 bytes apps/mobile/tool/icons/foreground.png | Bin 0 -> 23940 bytes 115 files changed, 9098 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/.gitignore create mode 100644 apps/mobile/.metadata create mode 100644 apps/mobile/README.md create mode 100644 apps/mobile/analysis_options.yaml create mode 100644 apps/mobile/android/.gitignore create mode 100644 apps/mobile/android/app/build.gradle.kts create mode 100644 apps/mobile/android/app/src/debug/AndroidManifest.xml create mode 100644 apps/mobile/android/app/src/main/AndroidManifest.xml create mode 100644 apps/mobile/android/app/src/main/kotlin/io/github/dariuszjaniak/warp/MainActivity.kt create mode 100644 apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 apps/mobile/android/app/src/main/res/drawable/ic_notification.png create mode 100644 apps/mobile/android/app/src/main/res/drawable/launch_background.xml create mode 100644 apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 apps/mobile/android/app/src/main/res/values-night/styles.xml create mode 100644 apps/mobile/android/app/src/main/res/values/colors.xml create mode 100644 apps/mobile/android/app/src/main/res/values/styles.xml create mode 100644 apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 apps/mobile/android/app/src/profile/AndroidManifest.xml create mode 100644 apps/mobile/android/build.gradle.kts create mode 100644 apps/mobile/android/gradle.properties create mode 100644 apps/mobile/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 apps/mobile/android/settings.gradle.kts create mode 100644 apps/mobile/assets/fonts/Figtree-Variable.ttf create mode 100644 apps/mobile/assets/fonts/JetBrainsMono-Variable.ttf create mode 100644 apps/mobile/assets/fonts/Outfit-Variable.ttf create mode 100644 apps/mobile/assets/logo/warp-symbol.png create mode 100644 apps/mobile/ios/.gitignore create mode 100644 apps/mobile/ios/Flutter/AppFrameworkInfo.plist create mode 100644 apps/mobile/ios/Flutter/Debug.xcconfig create mode 100644 apps/mobile/ios/Flutter/Release.xcconfig create mode 100644 apps/mobile/ios/Runner.xcodeproj/project.pbxproj create mode 100644 apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 apps/mobile/ios/Runner/AppDelegate.swift create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 apps/mobile/ios/Runner/Base.lproj/Main.storyboard create mode 100644 apps/mobile/ios/Runner/Info.plist create mode 100644 apps/mobile/ios/Runner/Runner-Bridging-Header.h create mode 100644 apps/mobile/ios/Runner/SceneDelegate.swift create mode 100644 apps/mobile/ios/RunnerTests/RunnerTests.swift create mode 100644 apps/mobile/lib/app.dart create mode 100644 apps/mobile/lib/main.dart create mode 100644 apps/mobile/lib/matrix/client.dart create mode 100644 apps/mobile/lib/matrix/encryption.dart create mode 100644 apps/mobile/lib/matrix/media.dart create mode 100644 apps/mobile/lib/matrix/notifications.dart create mode 100644 apps/mobile/lib/matrix/push.dart create mode 100644 apps/mobile/lib/matrix/timeline.dart create mode 100644 apps/mobile/lib/screens/encryption_screen.dart create mode 100644 apps/mobile/lib/screens/home_screen.dart create mode 100644 apps/mobile/lib/screens/login_screen.dart create mode 100644 apps/mobile/lib/screens/register_screen.dart create mode 100644 apps/mobile/lib/screens/room_info_screen.dart create mode 100644 apps/mobile/lib/screens/room_screen.dart create mode 100644 apps/mobile/lib/screens/sessions_screen.dart create mode 100644 apps/mobile/lib/screens/settings_screen.dart create mode 100644 apps/mobile/lib/state.dart create mode 100644 apps/mobile/lib/theme.dart create mode 100644 apps/mobile/lib/util/format.dart create mode 100644 apps/mobile/lib/widgets/avatar.dart create mode 100644 apps/mobile/lib/widgets/message.dart create mode 100644 apps/mobile/lib/widgets/new_chat_sheet.dart create mode 100644 apps/mobile/lib/widgets/uia.dart create mode 100644 apps/mobile/lib/widgets/verification_sheet.dart create mode 100644 apps/mobile/pubspec.lock create mode 100644 apps/mobile/pubspec.yaml create mode 100644 apps/mobile/test/format_test.dart create mode 100644 apps/mobile/test/timeline_test.dart create mode 100644 apps/mobile/tool/icons/app-icon.png create mode 100644 apps/mobile/tool/icons/foreground.png diff --git a/Makefile b/Makefile index 733fa9f..4b6362d 100644 --- a/Makefile +++ b/Makefile @@ -236,6 +236,29 @@ desktop-linux: desktop ## Build Linux packages of the desktop client (on Linux) -config build/linux/nfpm.yaml -out ../../$(BIN_DIR) || exit 1; \ done +# The Android app. Needs Flutter, the Android SDK, and Rust with the Android +# targets (flutter_vodozemac compiles the crypto from source); see +# apps/mobile/README.md. The APKs land in bin/. +FLUTTER ?= flutter +MOBILE_DIR := apps/mobile +MOBILE_APKS := $(MOBILE_DIR)/build/app/outputs/flutter-apk + +.PHONY: mobile +mobile: ## Build the Android app (release APKs, one per ABI, into bin/) + cd $(MOBILE_DIR) && $(FLUTTER) build apk --release --split-per-abi + mkdir -p $(BIN_DIR) + for abi in arm64-v8a armeabi-v7a x86_64; do \ + cp $(MOBILE_APKS)/app-$$abi-release.apk $(BIN_DIR)/warp-android-$$abi.apk || exit 1; \ + done + +.PHONY: mobile-run +mobile-run: ## Run the Android app on a connected device or emulator + cd $(MOBILE_DIR) && $(FLUTTER) run $(ARGS) + +.PHONY: mobile-test +mobile-test: ## Analyze and test the Android app + cd $(MOBILE_DIR) && $(FLUTTER) analyze && $(FLUTTER) test + .PHONY: complement-image complement-image: ## Build the Complement homeserver image docker build -t $(COMPLEMENT_IMAGE) -f $(SERVER_DIR)/test/complement/Dockerfile . diff --git a/README.md b/README.md index a31805a..8cf74a2 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,9 @@ same way, a process of its own, `warp-web`: `docker compose --profile web up -d`, or see [apps/web](apps/web/README.md). The desktop client, `warp-desktop`, is the same client in a native window (Wails v3) that signs in to any homeserver: `make desktop`, or see -[apps/desktop](apps/desktop/README.md). +[apps/desktop](apps/desktop/README.md). The Android app is Flutter on the +famedly Matrix SDK, with end-to-end encryption and UnifiedPush +notifications: `make mobile`, or see [apps/mobile](apps/mobile/README.md). Also in place: argon2id passwords, hashed access and refresh tokens, per-endpoint rate limiting, YAML + environment configuration, structured @@ -376,7 +378,7 @@ server/ the homeserver (Go module) filestore/ media content on the local filesystem sqlite/ postgres/ pocketbase/ test/complement/ the Complement homeserver image and runner -apps/ clients: ui (design system), admin (warp-admin panel); web, desktop, mobile to come +apps/ clients: ui (design system), admin (warp-admin panel), web, desktop, mobile (Android) deploy/ compose/ (with Prometheus, Grafana, Jaeger), helm/warp, grafana/ dashboard docs/ ``` diff --git a/apps/README.md b/apps/README.md index 03e998c..9bf56bf 100644 --- a/apps/README.md +++ b/apps/README.md @@ -6,7 +6,7 @@ | [`admin/`](admin/README.md) | admin web panel, served by its own optional `warp-admin` binary | done for the current admin API | | [`web/`](web/README.md) | web client (Vue SPA on matrix-js-sdk), served by its own optional `warp-web` binary | rooms, timeline, sending text and files, invites; E2EE next | | `desktop/` | Wails v3 shell reusing the web frontend | next: web has a timeline | -| `mobile/` | Flutter app, Android first | after web | +| [`mobile/`](mobile/README.md) | Flutter app on the famedly Matrix SDK, Android first | sign-in, rooms, timeline, files, E2EE with verification, push | The frontend packages are a pnpm workspace (`pnpm-workspace.yaml` at the repository root); the Go hosts join `go.work`. diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/mobile/.metadata b/apps/mobile/.metadata new file mode 100644 index 0000000..954d671 --- /dev/null +++ b/apps/mobile/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ad70ec4617166f1c38e5d2bfd388af71fda14f06" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + - platform: android + create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + - platform: ios + create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 0000000..0bb7a8b --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,158 @@ +# Warp for Android + +A Matrix client for Android, written in Flutter on the famedly +[Matrix Dart SDK](https://github.com/famedly/matrix-dart-sdk) with +[vodozemac](https://github.com/matrix-org/vodozemac) for end-to-end +encryption. It speaks the plain Client-Server API, so it works against any +Matrix homeserver, not only Warp. The same code is meant to ship to iOS +later (PLAN.md ยง8.5); only Android is built and tested today. + +It looks like the [web client](../web/README.md): the same tokens (colours, +radii), the Outfit and Figtree fonts, the Warp symbol. + +## What it does + +- Sign in with a password, on a server name (`example.org`, found through + `/.well-known/matrix/client`), a homeserver address, or a full user ID + alone. Sign up when the server allows it, with a registration token when + it asks for one. +- The session survives restarts: sync state, rooms and keys are in SQLite in + the app's private storage, and the access token and cached secrets also + in the Android Keystore (`flutter_secure_storage`). They are excluded from + cloud backups and device transfers, since a restored copy would be a + second session with this one's keys. +- Room list: unread and mention counts, invites first with Join and + Decline, a search, and filters for unread, people and rooms. +- Timeline: grouped messages, day separators, room events as one-line + notices, older history on scroll, links that open in the browser, images + (full screen with zoom), files saved where you choose. Media is fetched + with the access token (authenticated media, Matrix 1.11). +- Sending: text (`/me` and the SDK's other commands), photos and videos from + the gallery or the camera, any file. A message shows at once as + *Sending*, then *Sent*, then *Read* once someone else has read it; a + failed one says *Not sent*, with Retry and Delete on a long press. +- Typing notifications, read receipts, deleting messages, editing shown as + *edited*. +- New rooms (end-to-end encrypted by default, public or not), direct + messages (encrypted), joining by address. Room info: members with their + role and verification shield, invite, rename, topic, notification level, + turning encryption on, leaving. +- End-to-end encryption, on by default: + - *Set up encryption* creates cross-signing keys, secret storage and a key + backup in one step, behind a recovery key shown once. + - A new session verifies itself with the recovery key (which also + restores older messages from the backup) or from another session. + - Verifying people and sessions by comparing emoji, from room info, the + session list, or an incoming request. + - A message this session can't decrypt says why, not just "encrypted". + - *Reset encryption* for a lost recovery key. +- Sessions: every session of the account, which are verified, verify one, + sign one out. +- Notifications while the app runs, one per room, opening the room when + tapped, and push notifications through + [UnifiedPush](https://unifiedpush.org) (below). +- Settings: display name, avatar, theme (system, light, dark). + +**Not yet:** replies, reactions, threads, spaces, voice and video calls, +QR-code verification, Simplified Sliding Sync (the SDK has no client for +it, so the app uses `/sync`), a per-message trust shield like the web +client's. + +## Push notifications + +Warp doesn't use Firebase. Push goes through UnifiedPush: the user installs +a *distributor* app, such as [ntfy](https://ntfy.sh), and turns on *Push +notifications* in Settings. The distributor gives the app an endpoint, +which becomes a Matrix pusher on the homeserver. When something notifies, +the homeserver posts to the distributor's Matrix gateway, the distributor +wakes the app, and the app fetches the event, decrypts it if it can, and +shows it. + +- Pushes are `event_id_only`: the gateway and the distributor never see + message content, in encrypted rooms or not. +- The gateway is found with UnifiedPush's discovery + (`GET /_matrix/push/v1/notify`; ntfy has one). A + distributor without one goes through UnifiedPush's public gateway, + `matrix.gateway.unifiedpush.org`. +- The homeserver needs `push.enabled` (the default); see + [docs/push.md](../../docs/push.md). It accepts any gateway URL with the + path `/_matrix/push/v1/notify`. +- Without a distributor the app is still notified, but only while Android + keeps it running. + +## Build and run + +You need: + +- Flutter 3.44 or later (Dart 3.12). +- The Android SDK with an NDK and CMake (Android Studio installs them). +- Rust, with the Android targets: vodozemac is compiled from source during + the build. + + ```bash + rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android + ``` + +Then: + +| Command | What | +|---|---| +| `make mobile` | release APKs, one per ABI, to `bin/warp-android-.apk` | +| `make mobile-run` | run on a connected phone or emulator (`ARGS=--release` for a release build) | +| `make mobile-test` | `flutter analyze` and the unit tests | + +The first build compiles the Rust crate and takes a few minutes; later +builds reuse it. + +Release builds are signed with the key in `android/key.properties` +(`storeFile`, `storePassword`, `keyAlias`, `keyPassword`; not committed). +Without that file they are signed with the debug key, which is fine for +trying a release build and not for distributing one. + +### Against a local homeserver + +Debug builds may use plain HTTP, release builds only HTTPS. From the +emulator, the host machine is `10.0.2.2`: + +```bash +WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key ./bin/warp generate-keys +WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key WARP_REGISTRATION_ENABLED=true ./bin/warp serve +``` + +and sign up on the server `http://10.0.2.2:8008`. + +### Windows notes + +- `android/gradle.properties` turns off Kotlin's incremental compilation: + with the project and the pub cache on different drives, plugins fail to + compile with "different roots". +- `flutter test` builds the native code of every dependency for the host, + including `webcrypto` (pulled in by UnifiedPush's Linux support). Its + build needs CMake 3.13 or later; if Visual Studio's developer environment + puts an older CMake first (Visual Studio 2017 ships 3.12), the build fails + with "does not appear to contain CMakeLists.txt". Android builds use the + Android SDK's CMake and are not affected. + +## Layout + +| Path | What | +|---|---| +| `lib/main.dart` | start-up: notifications, the client, push; a push alone starts the app without UI (`--unifiedpush-bg`) | +| `lib/app.dart` | the app shell: signed in or not, prompts the SDK raises (password, verification requests), opening rooms from notifications | +| `lib/matrix/` | everything around the SDK: the client and homeserver discovery (`client.dart`), the timeline projection, media, encryption setup, notifications, push | +| `lib/screens/` | sign-in, sign-up, room list, room, room info, settings, encryption, sessions | +| `lib/widgets/` | message bubbles, avatars, new chat, verification, password prompts | +| `lib/theme.dart` | the design tokens as a Flutter theme | +| `test/` | the timeline projection, formatting | +| `assets/` | the brand fonts (as TTF: Flutter can't load woff2) and the symbol | +| `tool/icons/` | launcher icon sources; `dart run flutter_launcher_icons` regenerates the icons | + +The SDK's objects change in place, as matrix-js-sdk's do in the web client. +Screens watch a counter bumped on every sync (`syncProvider` in +`lib/state.dart`) and read the client again. + +## Licences + +The Matrix Dart SDK is AGPL-3.0. An APK built from this app includes it, +so distributing the APK has to follow the AGPL. The fonts are under the SIL +Open Font License. diff --git a/apps/mobile/analysis_options.yaml b/apps/mobile/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/apps/mobile/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/apps/mobile/android/.gitignore b/apps/mobile/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/apps/mobile/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/mobile/android/app/build.gradle.kts b/apps/mobile/android/app/build.gradle.kts new file mode 100644 index 0000000..1ecfd9b --- /dev/null +++ b/apps/mobile/android/app/build.gradle.kts @@ -0,0 +1,78 @@ +import java.util.Properties + +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +// Release signing comes from android/key.properties (not committed); without +// it a release build is signed with the debug key, so `flutter run --release` +// still works on a development machine. +val keyProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.exists()) file.inputStream().use { load(it) } +} + +android { + namespace = "io.github.dariuszjaniak.warp" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + // flutter_local_notifications uses java.time on older Android versions. + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "io.github.dariuszjaniak.warp" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + if (keyProperties.isNotEmpty()) { + create("release") { + storeFile = file(keyProperties.getProperty("storeFile")) + storePassword = keyProperties.getProperty("storePassword") + keyAlias = keyProperties.getProperty("keyAlias") + keyPassword = keyProperties.getProperty("keyPassword") + } + } + } + + buildTypes { + release { + signingConfig = if (keyProperties.isNotEmpty()) { + signingConfigs.getByName("release") + } else { + signingConfigs.getByName("debug") + } + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") +} + +// UnifiedPush's connector depends on Tink's JVM artifact and +// flutter_secure_storage on its Android one: the same classes twice. Keep +// the Android build. +configurations.all { + exclude(group = "com.google.crypto.tink", module = "tink") +} + +flutter { + source = "../.." +} diff --git a/apps/mobile/android/app/src/debug/AndroidManifest.xml b/apps/mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..1aa48cc --- /dev/null +++ b/apps/mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..58db8f6 --- /dev/null +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/kotlin/io/github/dariuszjaniak/warp/MainActivity.kt b/apps/mobile/android/app/src/main/kotlin/io/github/dariuszjaniak/warp/MainActivity.kt new file mode 100644 index 0000000..e3792c6 --- /dev/null +++ b/apps/mobile/android/app/src/main/kotlin/io/github/dariuszjaniak/warp/MainActivity.kt @@ -0,0 +1,5 @@ +package io.github.dariuszjaniak.warp + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..b11ea87bc77386ff87bc3bc4c112d08cc483f238 GIT binary patch literal 5760 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 G + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..89b9055eabf2e7be6f727fb887958e7832b21cc9 GIT binary patch literal 6802 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 E7t;mSLjV8( literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f6e5e60ebe89854b166e48482be91d79ebb230 GIT binary patch literal 12733 zcmdUWc|6qZ_xD(0iZT5=!CM;e2u09sH>KnT4EZ}iOu`Nm%jgs&b^ms6}iFl+O^2TU16@5K?tJn z0f88N7Kw{VAZSk3wFrbp7RnHT*t5VDiH_;U}AJF)sCYq?YkAP+Z*m_wOwO*rf7=qcf>GDQDQc?$%uj8Ud(Mg}@Cy z`b;NRO4LSli`}h{{&@UT27&28AKuISkQ%d8j-QmLjQ)94Il4!ALSt+{?X*qp+WV?3 zi89`ppB*JcaNuXk6MuCT^}CFSgB0B8+@dXMEQ%082~&z?VjzCd7J zaw3cp@KiLHQW5@K*Da1dolO+L3;CZueHyvZG8E2YFHTNQE|rn=1yP8?B-*CkRh~x( zg(08wysV~+4{uSFOZOTrUw@Cy^O>uTIe?OE4bG6*t0$TEs;Q~zV7uScOKZAKr_UYXH&lAeg}sb?_zn%+tPs-bm9}B2yk^IeeEp(baKChX- zM33=O_Kt4Et+}$>?UH!9v^&nbn7#pA+&vJ2@xz* ze_BMlTGv({KY4OU0aJIaB0;`5F^oa%AytNc%eAIE*g8&b6pgW} zHT~z1PO&4)S+1Rww;MkfJa&FR-C=SUM)u_bsKbR~kdZ%p3ih~`R30DDXKcm zgDfXquM7FZDgrBnJF2w3w^C3P)g#w$cO@|3y!)p^$nil#*{?E~ZdF^ZH$M(xqCqZd z3d%9@?9lS8ZDMtdD6N*0Q&*IHP81j+suF1|aps2+#jWnX$;1A9H;tZn)NA2F#}VBu zIXBAKP`5;zJ@O;LZ^t?_d+e1mb+wh5l5-zMrbd{IF=Q+`n<~4zM1!pNeBzn$KI}m2 zvz)Itx*1Q? zZw%{V^;28weL{l*#D+63uz5$;I=)hl(=uw3l{~GIK2SV3xA+HL>3EwtYbt(hZmz!> z-?R#z96o&bYv1&t%%fB|?b)`rwu>z15d*$A!dXe2$MfBhuB(`;9Xy5@-i)c6FDxv4 zIy*P#&<6^vM3pM_v#&6I74gjw5mw0Y@jV4c-wgiJ=er@C?F15c8J!0R=MF?ZQ4}iD z)Lf4ce)r^lU9IAMre95>^nS3aVMka;amPr{#{7;z5c%POGTs$8N{@mq+CnbQ>9v3D zss!u2vALej`8PS`2*Xbin%7&aBxr}>0+x+bDoI)W zM8RrVS)XZGX8Mo^*&_Q+!&pq_<-a zizYeutG@%j;_V9Z8U`^AA5XLDAJin1MhqHyT;YuNCWoKREx&9ubP(jyyYw5BunIr9 z6{g2N%I3|=ddr?+OC@I~(KzD?*WuKNLshw7|41{^hv5d=6JqO?mCqUorDzJI$59H2 zhreSUZd*PktYWHk5KLbvr^mFQuohH>DB6`v-)Q@=JvR7~evp$GHLjA(>rAakLOf$M zOMzpvU`@S(CElGTlY5bx*0mZJx2-p1I$fi8H&khfYG+z|xNuzA1kO8SO0y%1Np3H*@dsFhIs?gB5Hscr5m2oZImj|DlZM>Bp+l zlzrJ_>sVKXT`iy_Ee50+jdL42>yHKbFr8vGEUe4yj#M;&qxOpvKxr8Cd4q=I8#ecV-b^ zYapX*|MS*>$#&21_HV)YNY)R0u;~r4tJMi@}iP#LhYniNl4CqwnnR=58_;dFMZB zqUv#yi@EISBczc>yokyueb*tA^zFMvdfKBXv9qq~w!NCFsw!?LQoK&@cA8OmeR=N( zl9xT+EqLtP4oscEeZP|LZr(s z*uVDn?c0y6#0-T5e7hApQaKT;aWpQZ0k%Y9>DSko|WGRkHZ{Cj0FDr&#Km`5nP9(amHe98i-}?W1YMeBNwG#gJar z^_G*7_ZV-Ihqd@_rnUsmW1}HIxT^8AO{C*2|G*4ddjWefn`e4hY@i$QQ&V!w?H7j6 zRORybTY5KVuz59`^G7-5p?fWb2%6FfT{5AtIg*F62Wxf@&d-rK)o zi@hBgKWQcqYf}MxV-WeT!r(TUVKn>-Z9; z^`z8t)Z+nT_*G|l^v@ec91>sR*7klM-%~7(Pw1-Zle>@g8hZyXp)_%rQnBUoD>Pj#Ng}uF@dCN}!1$iV^>xzd#@| z2_S6W?ehqP9*wq1soPo-;Ue~*{K9_ghV%a;giE3hlNY+d=dfmkKH9%K+J8&@M|bQW zOH(7AF=K!6-QPjX5~xe5KG~qVVp>9r3w?~0a2Ql zNR)ke>RRsSWM2<~hwE*&=b~k!kxa7AuPDptuD-d&)!>Qhcc3E>%j#LdAdaB8Pb4HH z0>H7q2+C1SKrP@WH_!k6UU(tdH*aPW^UjPd-a(#Qa_`=~8!YFdQ>?08vM)b_=Ci3= zLZudAW8&F|2!$_VSIDGi6V>+OH)&pPtXk>$t`lDdvA8ptp6*O=oYQYIIjPj{$BsZr z+H>q)+_w=Osy(tX$Tu)Bu$)*psm5g36q8GiV=aniBC~a{GEN1+Q8xQ}*r`{UqzwFm-X=*wftr?L{-wOWPCykRu zRc`+G=;d1{o&Zqai?>5__rP%NezTqpx7q1lR!riqx66RO^w_d>;hc07hW;vQw_<)s zibpMdSe%#q^$LI}Bs*uXv7cD%+V$HDfRT#8@n^=e@kZ25JktlTh;qC}mD$)(X-$`F zFknrU@n&7Tn`MPMGt}jIJ-}r}997QUgICwip8|}3EUS!Heu_1cML5w+N=>~hzihL` z_6*_;EYso^!!()xWT0E35ap#iemeI1&%~8fa*2q!3@f2Y;1Ab@IfBOg=1==(oOg;_V;S8&Z5&R?>Caztd)_ zV?NHz%>4ZL@naT0?ibuSn9;_(^|LKy0jH@O9B{t*Q!rqbUtBIXgvrQT9gFbMn?{Ll z&PTtLv0IKs?dg|^2YWZJ9?qYkFCMNQ8*?jk>h@tRNCCLJofOc ziBPfWLe=-F*$bqRi?3e28e>ChhPwutnUeUElyw&x=I$^qkNO0NO}<-08Y#*m`b-Li zG3nI(yyY#hVy9gby_HK-y`SFfvZ+w}UE@|~sNq}=rfH|rWC6bUF;9AW9JWDQi zS&7%xNLzlNTeKR-3cbC(?JJ~Dph5T?^kBX1!@U#ZWt0&M2;c4t9B!I9DGIl%@R3Q~G@@?)towf6ys z=l#>}C&zd*7{V#Pqaxaq?CD((MlL~20$_ih@vC@G!MIG$&5G~K7Mks-qp-8M-A_rL zivdh-dOVl$>)pkR7bUbeZ$8tjc6raTfxfwV{rc>ERR02iHPh1zU`R1f2eaK~A^VP{ z?(baooa{f>U3i4*H-zZq@k^>lSc74jZ$ZW~`QGm&MJoG|aCxWH9mbWu;{8n~aEWqN z*R3;s)b7`J?N|tevm*itMa#8dUQzBNE6!=en?y!N&e!N=y{_lGiCp#rzVzw77YXuo zr7y`uf*$k16W-!Qs{J}xJ8OgnYip&}_e*oa5tAQzQxOY9?4a0;`Hw6x55W27a?9fw zzr;bb{nMcEJMLS(ns6Jxfd*cmfRKEH!dgnD@BGp&UTaJET@TwI0*o=rce?~v>4Cdc z^Te|mqaS90V+it-Znzeo^jWwG-rA8|%mg+`u=3!L_)Jg`&wl zYW>)qU-OoZZil5;KHL*oRCxaUdEE7up(wuhCAd`&+*iQl9`Pt|(Cpfisc@aV^|Dfi z)Gpyc@q?Sm3D!psu%;3!K1Ci)G0FJ%55FzHgn{e*ZNTru-Z0}Kl{=Q3W+KF!{GbORZgi%m51Covx zRd=0>Zua!_EFBeKL<4ZIaJS-u(ITAJ6XYEQ;_uUrpUEVC-$3zis@jBwC29iYh836} zDtRPV(Wx2d$kPN4;>5mI9kCF%(b7%nXU ze{=*au$7Y(N}LK+}Vmx*UK#eY14opRLdG%m55NFVrd0L;g^`R&_=r1u#bkF*# zhezU3ap?oisrfqgLYfEm+A5dFpVDG50T~7b{oU{OjZYCDjO4DDwEEsN=1drEth$|~ zHW{__o-`65ZI$`z4W`1C5);6>xnceK8*u% zgAjXzqKxVj3b%Ck9qj-LLPq2yArk^}sOSaU-{TB@PI&0&A3uJCH#Rh+m-C)flz%xe zUxCKdMY5)h2!#{DfiD`L$|Ss42O)fy0JCA6QmLVAz}vPilf{D%UFONki8W&*BXg3% z?7wca-_F**X@w{``}60|ukU9M>`|=Ku6NSSs(&!z2~1=8=e!Fi+9~!-B9}y!6Qb5! zlW|e7Ew2ygL2-)!kiYua+}wP(aYINm=rA&q+WZbRsZ{h%o0Cyj*AeciBw z)R%^l2Ew~r&*nSns-V+(r`{Bcf7{%0jVc^)eZ0p;l@@Nd%wXNFn z;|15~{Yj|)m%zn7B`l|f8GP!HZfeGP-yQ3y_SgY4O@+=F%J!jsUBRcW_fO^Pq~xlA zHwNUKXD~rwV41{I_mY#7-J{oJb7)){FP(rCgI@#P+!F!gQA^G}!mn4}4CV!9yef#O zil#GuZPO&BQtbh-EM2C)yq}UkjOh{f=rd0@7)$qdzh4p1t5&<|ve=E>+%v%X4R&)S zR$vD+0-lXGywBJ*bai4|VB#@dSzt$=oE`7KoPg5)g7L&p);KD7wBU|~K`6`X7@3;c zD!Au9#42>owVrmp0j-@wL3UE49a8KcIzJ7#g9Rn72893=Uy?G28X2_Hj+% zIjR6^Y0kPNT6#xemrk+{EWCd!iYXWN6nGJKy{=-uM3rty&evId$t0|PIfOS2W?GGyYX;R`x~hBP+3GYS z&QlkQ%VGse7SeNxL|q3ls^h$Lh*g#6(v_LkBKEYWW{S=9oT2O(+@ET%FIooUHPj~y z*NbW_me^4b?xi$Zj=7f{cI{l`eEohcYlbt>qqOuhq&&dznd_X*VoU$i$VTJL3nlJ{ z2vuh#KJgQZM%5_(CoR1HY%m8vXou_zf)sK_IAk$?!+~!Tq5oK{sNL^~4pVKjh&wyF zwkFE7?8l!$u2mLmDz#-_jy(SRz-KUCrO2L)G5t!6F#F&|qTp1TRqZ29WNc01(X+{@ z{<8vo#3}aiSC>XAj6+E%Nr!_D4qhbPtn|U|cUat?B~rPNU+DT_`t5BifzX zmM5M!@Fh%+o02DEv_oeizNAU`l)eD9q;n6*RSt4WiTjtwPRF@Od*AxI< zsd+9fxI0(2sT)T0t(rePGBx}3*JMQytv&Gl&!44(YFr7+LA5q33IXc{{_KA4p@)^h zXhX`V$eTZ6{8E6K_xZ3`Cq_FYu*{d650JRAUXBKwcqTbK zBO{}OV>f9%r~l5Y-ofl^_d85bt>B9q*PgTTDtp@Ds4@vt7X;wz67bdXkkH8U%_4kH zoXN`$Jy-dBvggZY@}za$1AHyQljlJip;XvSDxV3?JjPk|@ynMlYndO)m#Iqe%pGvw zl_afdaT4UW!GDxG7qLUno}JO@5e^8SsS0q2mA{4o)@&3arA)-_AG)td^PK6OKGh@4 z$RaKpHfA&-X3VO`Vz{I^W#Jkrq)W=+R`TSFf%f^cJy#Vu#IGDXMDcxh=`GQBVtKRMNh9jZ%&+BM ztTgE6CEw<1A971QP{R>@ou!FHzl*42Vj}YR9G((&2qh^5T!xT@0qUTDKWehh#*WQB zMLQ-_<#vnGv?oSGCk3i2&(oML0*O7NHzL44PDL&Kt=Y+d5lUTNg-(h=8Hiy?$zuIz zvw9L07+2jKlAOwv9c${4f^7_h9}Q5S5wPOG19k2x2>OTd>*-9mgvw(lN>rKA(BA^g z$~YQRxq7d`J@>6KHC*2#0#;c0h_^S9|&ioX0tSUSS5}<9ch4#q` z_ViU??y$nl_!q{7*hE@Y+U?u755F*slt5#_=!(EY%cD#Fu&yP_icwV@6>!=Bl>Pj^ zKR@EY%j72p#XFGsuJ{%$9S(?ua zLvxC+@9+iz1{({z72j#+sKE84j)sv6xMy7m=M>Sb`l^*=6$dDvxIp>jIu*HGU@3Ip zYH)hTI%5L1WMh5{%!T5H@Hz|hvR3u8e+5crA47iBrCWL(B8>i3($T7&A2*(aQ0}_V zwrEfekr8GxUgUKtKvn4?d%6P%`?1_>*LEF12AK-gnwTT;s4*x`If6)uB9We4PfSdF z`;T)GAn32TOR^|@2l8U6`lIbEXj%3a_&B4(SSq;7prG{WWv=<}t2`}O(N-`rP z)~o+$hbB? zcJ?~&ND9lVsC~#D>+k|AEG;&at=VR7ZoXFX`C)KKkavyTp8lM21o&pRqTC|m-{N!|fta$XRj#)??x@h>La`jE6!?e;_`e9M`|x8M^N)m##idua z2p1-x!g|3{X;4ifa|P1Hx=zVj+A44t+C%Vfxwkxvz>HFDJeP_(=9QC@XF7h7$8I?} zLFIL>zkBbVPyf>=yZa$*)T2rz0hoN!Nx2TmTg!~4S^E2Xj7|@1SOrQmx@dfgzrJr% zmmS{NjZ$*I-S5gWd0_Q3Wm!t1M)lDmqoJXh!AgNPlkpVGTy#YSA<_P+Nt8z%<7)dv z-Yjp%&U=3y4(YrOd>hi`by<(!vvy_q816hiGIVqN$8FK6+@Ku&c+}@Q?Kp*13HorA zis-&o)08Mp(!GU&*%|hdbj+rF?fz7H^6Mp3luSJLBn|_V^q2ST8LmKbG$4fM4GjlR@A4o}_*%jgv^-g3uth zNsvaCHs%`#j}4(6e5c@P}S22AQU4_yB)sjw8gT)JD~-b(g+4|4id>*a~F&_bGT%hm4SH!q}9B@4PROKP@E9u z;{P;QZunMDuv{+|=Jt!Epv97&kx9ty6VT>wKoC_QQ zA-j4JN|rK$)f=`=$RLxmndK<&fFO_XI1QxQS2gjXJ=idmF!A88DD?o0c<4)Lb{NYN zar&ngfF*bi1zaxpF{(`QMksXBhi^h&2nUbo0-vy$M76oM5|e)4xkyRb`wUdU_3n9= zFQLdJ$~x#wCtAeARyKpKU^E94PsM7R-#dN*9vs(?ZZU;!bTdmHBaA?DqDn6Cvz z`?j!_liRD6`djI~mID2oPFoc9ph#{%7|eN9wZik-*K?osnAJA;38>rjToL+K{vj4J zSnQfUH3eN-j>@&V>l4W0a?FzBDT*HQWD@v3B+P|fWY3~kIN}wem)cuTc@95e?!Ch-VTVKrH z&R;JQc00hD!^og@!t^s2iMdsw--yua0SRVzOO&Mj<#4b!f9G z;J{3?gu;>Rtsb)yl+hCd0|QS!owjJk=@vmh(1oPGTaJXL`Iw=qO)Sj;d zP57}%sRVqA}hvv=Pj))s4l2mLrU2go*4+y8@kvbn59# zCh77$)Zl^{e%j2aEAX|e5?*vOiQjr>5_R}<6{3@zp8TzZA-5gobz_jkY8 zXuBU>!68+5)=bc*+ftW$2L{bLzB4!U#13t zOM$8-M4SP&=CG%uHA(yN+5k28>M=j0!^vXEwbm35a8G%Csn^9S^~<$OpCQF06oGqi zE^6QN(51Pvpi3;`1hp|}2)bvs2vJ*+%B81v-dH+M6DxkSnn4VBJE8vuWk}u*kw1Z` zTm9*Kq9~~DMH-=cv~N(Ql_X_9?zqvC6xma=oA6sjl&;)ibY7-!=7a}Y)zY%Y=El3Q z7UwA>g!8MnkQ{+Tua)1!ZM$55Y}^~BBKmfKqhJG_2$Wgi?pj(G;0qiJ4JGarjU)Ix1e2f&uddb+0P~=C4;GmQUSLw0U{vhxNd2MV4+7 zmr4fG4M6oB-t65C7Je(pVPB?m=}@;#G#UadGJ6v+rmVq=*@3Lt_(^9`5Gw_4x&?@h zSnA#q3^~&TZ0Qao4(|0O`p&^_iA?cv92#v%+XzMO+lI1At1xf-A-xJEjYNCP3e!Y0 zJ`};dyqh<>GU(ILO_doa0ZP*#XSx8PvkyCnsU9;<_JxuFV&#)0Xk3Cm5LscSCzKeu zP!1tdqM&Z8s7bm4d4M%bj~VHWk_-=)`v#4Fz_f+jgKU`jA)&%vy4Idz$zLlbvG+QG zx$Tqh4qi5qmm5G*7pl?joU-KqIwnx9f~E^Uz=0*uV00jh2$a6q-dp^eP3LJ=_kCHd zhY0@Gcx~vx*?aY77IDOd0;JA6J*4s-f`aWv<_3W0TAHRQAV+Lno}3bWxC8Qe@Jb(6 z#*zzB`KEvVITiZQ#XK7Z5CKW3f>z=I4R9b;7=TpaD*YgIJ7r1&iIW6~PoRQ}18M35 zw5O-0qV98j=RFDM)ZQMHyv5>3Pp%c0Dm=3FN+YDeT7<$96qm%OA}YR&_GKfLk6X`~ zKJ?m$tx>o1jHSvUAWxyw#O^q9+!uj~myuhis=A7K!rpG*E&~7{O*LQn+wt+H%YLjavQD&1dWMe`nDG3kQ+vOTAG`4fWB6@^Ujpu zaI`hWr8sRsU=Qvo;9ep&^jxbJX;5kOOQql$tTd?h&L64#We=nx#{i&?LKEMBHT^e! zk|a2Q@F>k?1b1j4&-DE}>NVuaHvuO$avqq$WmklF_@!I2ZN>O!u>akPE3AHggczCx zK=A-J?6}$S^}%D}!;-8in$bx#gaNR{auUlq@=~Ki{}s6Ss_p$VH}?oU5UQox04D&q z(3?oqaOm+dTYsV9=^aMuufGpFHRX(E5h-qzydkxB2aw9O+n{^l&+*lc($dmh(AW5z zNyJu1pFRxJ=wYiRTBh-#+QQ5;(4w0C9%#WcJ8yKX^L`NKA@nA`4)G&_^1>ucS2 zr9ajZm&)`W_b-gqsCY}FRuTQwMLkO0Hu%qh0TM1~4E`e3rW|lvh$s<+wt@a5w-1>- zbkg$I;e>L^UM$;Y z-sn1SwhJFhj`Rr>urj>}h0uHgeQ{E4VvY*+_rCPl;=@(Es|A&3B8}rE`pq==VvV)( zP0oB?%XO~kmW_}ExKjICCbsvL2J*qS26y&+%soa*p7KKFjI%S;LMDiX3QJQYb9;G% z>b#x-US|!8dl^5egT~q7W6msnZ5&V^@FA~Wvt}`*(CK)hnn2fn^U(wude!*4NJ;vIGt9c=E-xL8qZqtGq z*0^f{lvg{zSI$V@SjZyUp}1T5pG}aMeyXM>8G$&(wcv`<1CzH(3?h}nvUI>-qPiJi znf_Woc9eck*tlNx7c1D`PW6dqs@ zlj;pLb(st6aLca01HiWwNJNG{5NQ^Be0!fFRL?leSFw?};Zbw9JQ|vy&#E+Bl}?X`hv@!xQ zKl;xPhlZ<{p&;u(_-#OZ4V8;DYR4)C5jQtRe1W_39#Rt8Uv1IU@gET4WTd2}<(vtt zS7G{P?ilxVKgpSSg#?;X1xh5M{S+sjeEb`j^y2m2-wB2CQ2sBy73K6~h49IVx^jl7 zi@TRu@35|vms`ZB416BDqR}3kVgDk8Q^qq}?_J>&x&h>EJId>{1J#XQC-mBE$LVdy4Mwx^og;!xVW)m=c1&Q7^!665w-b&j!Q%VTJm|Q+@dRZXEDU& zp~xfi0J`$7UahE(a&mSg+_b`bRQ0+Lc9DUSMIgvi?2OmnAB>Q&gVCcmR$i$bZ@ol3 zDfmPNsSuB6GzZ|mKpWGuYA4;%2aHZIAxZS;yXP^@yQO0uK%)!-u{OWm4|);0H`Hus z{2fd53t&yT;>!Z0qi1hIuZJro??-KG7E-iHaa%`)p3wm*3af~b(&K4NxV+2{J5{EZPTAgVGx!O zna3Kg-nd|tq{H0lNKk#ixL@W_zm%j6_-xzMX+u!3hD0%2jzpp0ouI=dg!!S+nx3Jr z^k_fsCPWcL(^U}aAW+2?hK7N|@Pi&Lc;x?|aNGTHrD&&nK&e}y9`&?s;@O2PVqVyW zMohjNr#wttw7x{E`o)D;%G(>N3NHUQL;m0YHvs?P2mYUb(!gAj&V2P^x<;3D3IRX+ MY!BieSd(M_53Qh#AOHXW literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/drawable/ic_notification.png b/apps/mobile/android/app/src/main/res/drawable/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..a41a4145d375274b661f1dc666dcfa3ac4309785 GIT binary patch literal 1235 zcmV;^1T6cBP)j+rUR*hEnlg_!oxg&veLizF(;C}bdo zqF$n)1{8uYdWw3f@FgmZB8WlkCc%V=bWu^lilpetvS>8<OW}{~p#E(DCfG);?=r zrrJL^PwU&??d-GGx4yLwG&D3c{Ex9z1(GHKmjOL_9s_|YeCX9NduC$OZr=# zvscnflIGMMQ#6uRN;;SjbURK+dPUOoy5q}4(nLw?3l4lN-%7f??pV)2l1X~215y)+qzSlXc4~z9N#F=@*vyn_8)`~`q=i5~umG3?OvtHX zMAB|x6R^(A%9$%9JsDUA`*P}=D`{&$XI_pyqV)y%d>iyUe>f(-m{njct}RwIYa;4>UyxCiVK4z^b_l4XLwgE?X7*kchN9{K zGaCe+DcP4%wgIuNw&da}x)3t6HNfM*U#Uht3<0;B+3zLVIE`GDpqagZ+rqY;nG-kw zJc0}UNFJ9`4d?C$nAt8#*8z)wWx!(G{ebSAs)m6bzy{!5Gdr5smm=t>A<~+iw{S{V zSklxS`iko4RR0EKVP-{ih;y4wRgm1%tC{ih28>109l(oihDMv9OU!IrS@vTo^IV`C z@O5XV6ky|Q&sfw!ZJ9rTUjyT}r6`xw4J`DC>`T#~jHDTIm!Gm2k+i5o-6m;mU^Dej zjIPpt0%mql(oW!p43V>dAxUoo+lk*wPXU$#*9YXz=p(eF?6bi)$W8#bdE)-or)bWN z5h{BbT#sv7)WN(Mp;8lIW+T9(xT~>MQ-1;g%9Xy#VYZ|qmfB*Fv`W(Ph*nsU zgeJ=BN?|j54!9ZkrYf7jRUv^&_X1}2G42enA9x>k#jZ?(38?jkhommtK}T;6U;e=D zB_0Cq!(R?>HnUrkoE*={{cqu6NqaKpfY8a-e*;Nfl5UYZCm8z&j+u4)S1FSEo@Ns}Zkm$bNUUm6-38XCqc{{U!o2f + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..073b498 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..674a744a39a8c89b1d7740fa53162e45c62b294c GIT binary patch literal 2642 zcmV-Y3a#~tP);CydMIhfbtLl1;pAY6|K+suH&ONrcIPi5_Ot3nMq>vN7KYkbn?(? z>li05jZRD%(NtR?5)~6;f<_#9R%8)G-Xge&yq4QJcUjq8?mq9naPphEGxwhRo$q}2 zLvq|A#6)HwG%yyPvOo|>01yDIK@Y<=7s;>#=ziFWJFOMC zQCoy7p*~s;j^)0^H(UVK7mh9Y(wkaW@FO) z4pMmeVEd*+NSwTCY|R!cKtPZjy9RIWn1c^;evPZ;XZgfNJ1EFM07s*%9rPVUXO6}>i8`~!jS1LJGK4VLu=D^1hX#@V&BwZ( zMjS*4ux-=f;hFRJEh)1gz{V%{qoKJHW!0Cots@RvlDP#_C$Dssd2Ru*NQ&n+eu3Bb zrl3>RiAN6dRRnUo)yNukf9`OUjIVbjez#^H_8$H%9yy4{Ihw7UbsJuArA*q?%}9zp zK$jZBMaY>mw&Ev@fVyeb{MS)dy^KLh{Cu4V`6pOs`^+((V7lahk_5P9OHc??2> zV$G13RfIPqa2#GtNXN2>NyJwoeI!F&!?LWN9>cbl-X3!7Rwesh6OxuXTBC$z(xCHlsCcz!jtP1zzAWnl34jP_9 z7U`K0Fb*#zW?*^5B;v(LtO_PudV7#lS4G~bC}!)8C%9HWmlh6OV&>r^@xb3R7GQI< z<`tYR32vxNjvWc<*gAO{{#tqwd&-NoGs-S-83*}Fq&Tx+J?QMQ2V`O~7fYoEgxWR1 zb(n)pK}-lvX01U+V3^f2#vIm7h{vtzU3jIW$S(Q>ku=9Y^dkozk``$$dG)+Hbr!Nh zC-C;OHsj}OdpU6?@*0)oe9LW{o=fLk7)f^y^dVT}$xG5R1H;IEl#J3tYe%mo{fw8~ zZO!-%+eCzrJ!x6&%)HNRdM@1tK})Qv>n?&r#`E5LDkc%%w>RR$o7XYO4n1n$7&QfQ zvEvh5%5C;Ya0NN&TuVJNjW|e5MA$!bA>K`&hwj1tN9_uU)TXgk7oEVRwnpxp{+Xk_ zeX%Dk%jozNi^7B_xPmz7u(FIRm`Uhc3nRQBXC#o1ltgUem)hm1z#_k2q|o{mZ4 z)e|W7wCTBY8w79jJFi{9g0LvWDg1@1;ze%#4dm4S&*=R=weh5wrY=Gu*g^LP`my?a zE-ozE#Fb#}3CbVOUdpqpPHSP8K(Xq62WA%>#j(s4NYyWF*^DE#<=9qS2$c@Oy8?o< zc`d3{_Xv$Uo{mYvmdPoY6%$4FquA74?@}9iIR?1(lyX$lRdm-Z&+V&r)-{LX2NZ@F9%s7 zbA9M zs+Jd88wA_ybO(J=SA{pG&4H;TKq?Y*2YBk#*3_+K`fo;Tg40R*rs^I(skzP-+2Npy zqV_t)KLn@rLk;&QX?pU~kLNHqBoeVc0S>7sYOhoD%N=8bq%oY}E<1OeOK|D-?kFVt z3IF`tWr1R~>K9(m^mOZ2_2R%Y$a+2yF%0_xA;4!iT@#;|x^mC=TIVvRV``Jj<+K~t?&eLkqZhFL-3*b$$GwG-muA<_>Zgz^vr z5L%J)5TwP$ps$zc@y9D?@$BSeZn;Lv72LffzaZNcv+N*)E>$CZ6;bvx*L9$8I^Hh- zf%`&#Rb(vvgcwOeW>DY2JqGnn*O07;Hn`;!l6I8LAj{%-{S9)w{sy;`(M308%UEq4 zwG7H{79u%rwSj=sNZQddG@P(mPu-mo2F0bh*sydT2oavRJZ`8|7BOh+Xhv!ENub$nNH0JGCxa2M`y zsf=3Phup$l(96Y+p5BS1xMlG5ek`_pK{#`151QNRpywdfgZudB(G7Sd`vSzGF&#Jx zLPPTvE5z;Q>BwI95#pm38I>E$P!ILt%y)Y@&e2&`x-eZ)Y-zuPT?dw6cIq0enD=Ld zhom{mR;Pjnfi%v^J+lkV?RBO^&6iwh-Raw_C%7YaVk%M-mm@wZ6Jfzqpzxx<19xYh zVgYs#y#x2qdbbw!jU}k8Eka2}9t{F{bd19P0n@K+_9gmz_y7O^07*qoM6N<$f?hQa AfdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8faa5b337b02c2eb4a8da4aa57dc26d8b1f5d937 GIT binary patch literal 1617 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 literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..97fa27be0026cd73efe7739a77600a71738c1e42 GIT binary patch literal 6399 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 N002ovPDHLkV1f+D=EeX3 literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/values-night/styles.xml b/apps/mobile/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..403f795 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #00A389 + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml b/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..f9adc71 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/profile/AndroidManifest.xml b/apps/mobile/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/apps/mobile/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/mobile/android/build.gradle.kts b/apps/mobile/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/apps/mobile/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/apps/mobile/android/gradle.properties b/apps/mobile/android/gradle.properties new file mode 100644 index 0000000..1bd539d --- /dev/null +++ b/apps/mobile/android/gradle.properties @@ -0,0 +1,10 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false + +# The project and the pub cache are on different drives on Windows, which +# breaks the Kotlin incremental caches of plugins ("different roots"). +kotlin.incremental=false diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/apps/mobile/android/settings.gradle.kts b/apps/mobile/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/apps/mobile/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/apps/mobile/assets/fonts/Figtree-Variable.ttf b/apps/mobile/assets/fonts/Figtree-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4645df9e4b41d2852f7d7fda9b018e998db93d02 GIT binary patch literal 51284 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 (~t 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�W5buB>0KI4Gu29?PAXNIEC88P zGL!>D6|RvXkqC52W@Yl1Nu8ot==3K}BP(?AHO%EJr@Mkokz=7FPg4X7@gj#vbg}cP zy*-_FJ4F&ef@REOrhT6YoTj@8oUHvh=lROjKe5wh$FbK=pRg@fiS8x7k9pLoT!vu16ku#O2FM7>yzCE)pisp!tU+OwOyak)n+K`q^MKF+DsC zs*H@!q}ei%1y#!^cknrcQ^SGzl(ga|K^V)!KwI2{}r%CS0pXAfhkHi z6aKdW0rR$>Fm3=-Y%t<^kS^S>DqS9==5sgI>yrF>V_<=-@+l5y<3_P~<3^cR5E9(8{!y81Lw!9G{+zs_xn+vQ1-;;TUXNDWQ?|q+NQU9%AGje=Lt_xp zi6p<6SLX5Xd~9)p`QmD&8>*>cXz1R1BawUWH7t@RlS#F+Zqp{I!nt@o@-H6y7VOMy zgW=n|b`h(*PMx3FCTOs=NNhGqo6XkdxxYf1An`yudvy8Wc_ovTlGI8(yLL$^DTf9! zvi@fN=j~%M^X`ArY_4c(QmY?+n5gm^ZXbmb_>~h%r*ncgIf;I2*X8=n&-L5EL2UF( zY=>icb8}G<8JvN$zuRlLy|;Q)AR0l0#E4{iI@iDNQ2qM>Mf2KGLGoC+#JEi+qRkhh zVi66V=A@8E=Qx)xud9K0wQ@NA(#>3MX=y?>xAGyG%u-#=;h=uW5^s$-GPg`zQqarg zayScX8X6i(^t@;^Ixzw2>vVdplslo0#_}*Zv7TGURo1UczU7e^pWrj2L5Eevoq*;t zPESlFN|!V>t$>P#JhD>@A4?<_E*x|5$UG^w%jMp^+u>M2Mi3t$R(fl!Az~Y?#?~T2 zgvjdqu&U~dM48jOojeB@+jOEab2w6nnTxdDN{o#3{(>{-Q2AK7fvM$bdkE&0<$cSw zOq;~kMx8lx=qO7m`WYg`NJmG<=p0o28^L)R{5s}>l7>j6%tWU0bK4#HO6mFW5w)C`8Yk=dz;QICb(S~^(I?r| zRIA<{kL&e8#7J4c%7ppsGts_l8W|(ri(Ym|f`OnOBCH%?bHld*3wJoePBe&Pl0>@$@e<7|2=IR!-9X|~ega9H#y zoSqp2Od;oafh-@}l&36T$4j;PlxKo+L3-{6$R6a{0BqH;h!5ZpUbob!V6j-mp5_M9 zmO}gsYnP{5OB|v`nHxo2bEZX+5T1F`+LZZC7xwl_Ef#6^H(e;N@KneNj=jdtnIceM zCpV_tt91Iph3U!%9;nQ{7T+AB0AuVROndg2h>q-7a~92evMnnW*~kEx;LvvVH=U-q z5@Mw1=c4{8S)oZg-_rwIr6fCiq0HkUM^1Ied@iIE(Gnv#;Dq>vFoQHM7K+C4zj2WZ z1{oKsQr$@lS(X+Z=-E<4bS*{aD7tXnL}pLU`Os;_8@<3ud4WJ^`6;iP==Q#l*$?d0 zbuKq8!v(UwYc@j~aVkBKcvwSX@H(Sht1b6(6$J}0T?+~n2W8!#d~=IFiGZ7=ehIdq zB`9^p-EFh&PNfPPcT@Ak*xgu|izv-m;R3%nnnYEmzUYwjE!y*TR38?Zm=R1w=L{xdrV|s9;inwmQ5XaS-ApuiiBTV|7JJN# z!1SZ$E5{M`W?Gi-w377n^mJOHEvSIjT2X+oH_6!86jP+OL$9=}Ma-!&pMXy;GAu7F zF$UiviS$JzmQA}iH6W~{Kjj9{SgJp}I&BmsZSL&zrNc4CKzF}1Iqobp$;~9htoYa` ze~SmZfEzdxOCLFP!6&^u%(!s$jBcjSv#czy(gW5I|Nh_pZ@mAcR;TKXvVQT$_ipR` zO@8j1s4$O?YPB6B4qZ452qPfal2t#)OH0S$#mKy(>gV+StzBIkHhkE_pM*|~?{c^UqtOZ{28-*J6> zw90++`j>n6IB&f^H&5K`^O5?4?R`u|rx*7S^jgeh$UkyC!)#U`oD;XG_O!Jia#bq7W@mjUyi{?@l zY$rpOgD?^yU!ErdN`+)fC+Bea$!Wb#DdI8`(>_`0wU1tX_0^BAm6n#_Tu@d@aM8k& zDkU#{y`!u+AFq?6R13LPor~Lau&1J-k-_j-2#%M*kx+CB+BaLiz*SUaRRGP^T2$m( zD93xqhV$A|6M#?}8^d5tk|JO~X<_7>FS2RHY??+UAYu~+gmfQt7}9~h?v6e{1orlI zboctxhNVkuoAx&E+_|;Bu&{pX&YjJVH`OATxdhK-u=p`N27Z)^ynHhF8z=;wDyIoH zKBGp)o|CLWxEy8EzoYk@i3!epmw#v|I42Oy1&4%-inH*+Bn?o0i$*ipq6#fbJ(#{a?BH)CoZMJ?6l&5elgUc77v zT$*-9psNp+R@zS;0_Oe)r_T3`XzucsPkwJ1Tck5BM462;X$)SC_5tx)ME$*n9Gibe zNW%X^NW%Hd_iMyUtbHipy73Wg!w1~Nuf1H_`A^P5jeq|)jp^so_h-KUN%WgnPxRRC z_6ald3B}R5kv)+cS*d5o*3FyNVCP=9<8gSy_V3%=J;`U&X|p(tC-_Uktr4w^ zMT}w85wQ~#SY0cHjQO?KoqksOr@#K!?|%2Yquq1hi}~)hKmU3K#&?&u-Rb+Y`)7%p z$nZ%eH6rC*=86z_<#(F} zx7#0_p^(l^WM;)cJ(t!9q4om%*$IE8jd|Kq#-`^}G!_?<2JV&QRBSvv$70O(L$~XlN^yYb?XsDsV$s>z zxkM_BL|cZKH8VczbjnjBu*8W+QgUZ3KR*^rr0`+PS3e)=BeUBt^+x8odh9f2)1pEcsqOs4NL!0Un3|f-IM=7ACZL87PNe#Q#MztFNMUf&ve`ysYb#J@;q097 z*0;8{cMp$G5nKe;@`#ymcjv{n!I}7tPv3asjidhYq#lZNv8m&7YalQ*IzBlEurE>4 ze<@I1EgwINlpFE6acMP-znz^U;rRS~C&e|pDGEPHQMjVBvmadv#b)VZrK-CxI-`Qk z(xq8I#BLzVC=eFsLDX6$j8uPzkk6u#ux~1Z4waHgUzwE6zIp))e^jZjTKT+!DU!>$ z!rp$K%2ErdP+vA4hn#uu_*rZ|{V}@AxuEpq`Ehn7a*#Haex7fVG)M(D8*DC7&;#^!3~5!dlo52Ow^T}ohdEkI|AY<2 zmB`ANM1J|)cqc~IIX?H$Ln)mbZf>_ORbGGkbo~!_ydOYQRzc{&(<>sAg-}=N#7J6I zgCjJFA4dLA^!_E)mtVOveIe?nX9BPkpFV%N1Mnju$oW>OvwSjwl!T{ZIUsj#v`$rxokxme&Y)wfRl>elUoGT_M0^oZTtr6C06K;Qk|vwE`NbeD4w)|&9sWgn(UsPg zmcHoJ*uYr-g-<_i4MgCKe(nA4$fg=$r1!sHXU)d0bo7M+M~j$Tm9{uv#~_$Ywm@sf zhGDa|eE9n7Q@bI>_wC{{m?{BRWv29@b=2se%8uS{Bf!TQYfGjx@hH8>*AOFiVNj8pl3wkfLTeX_&m-XTc48{vDQW|XT zBwL%MxlD>9Fvd<&9b*RHp}=iC0+%{hz?~mDeOg{#J|ktZxCmBJNx2?Rp|C?(NWk?> zPDo}-@IuZS>wt7>80($jBNm6>`+kpkO&I!n951C*ty5q*-!#Ajzzb!wF>;RoU z;F2KHh2FY*A+iUv)DlqCqU=t&n5F?2uBB^v5-+oLH?F7K1Ul-Df1>XAXE%-9A&F&N zU~o)N&(04Af=DJ!vU$3~qR=>lZALz5HB!H<)u<1+NXwnM4%Kf^kgry0u>H#g`LO2z zd(gXt#^Ufr;yF5>o7rA;Qb~6^!jA&eX$0qWcZWFIVmEe2cd?e{zuF^~h`1aGlb*uD z9-l?Z2qU;=aOmRsOE4z~X+mxFvTN7p6rOK>^P3OXS*=SSBcWdLt^Hc1a_aaW|MZoIYZ@PscCs{^$6?FOkQ;X62k*iU))HYj8yZ?#1|%g-`zRNv zd|y+Eu=i}MQYGPWSwyO}p`kUav#g;glS7@b^4Y0*psgTxFV7f^(D?c#>(;GXqURIg z8+lr3JYG_Qdbeim09?LU0q>WT*WLy%T^9hmm%^gBszJ=9!y0kW%AQ4ajgG;2o=jst zbSlPk?Lr05T_wEu*~0}Ic}oj?O{lh|pp(ExUzQ5?6u99|NrAH2Y+J%(rlp z^ry^?Y1jAB=uf;}8J#E9z08It(Y9Te7wjeqxn&7@$L8i_Q?rY zAG=a&ThqOxA)2xpR=ux1vi-h$!624Xr;sqLxvjpN{mV<5r3zudJwTpR?y#;ZgtS>K z&RK>S`cGaU#FUw9a7wEw&~ie7JdGUCyp!Y)5klk|oy8LfWeD%Yxj)bWJ*zh%$g4rm z>Z?*|0+x-jkkc9RIcB4M7vQTs-xr;uYYcCG7??9{28O{V<6Q5dH}neT(WC8Wj~?x9 zM|DjN5*CH$s>5RGh%a9rr)DXKftAY+*2Bs@ z7-tC7dU&yzq5`Nik%&$gi9pWqyt?>g5+*#Yn122ul&>qnxj2EcMv>Ceh);~wbAzSN zt6tGPBm!eMkeotzd47L&wciik(|P4_o{WUluXY^&5cH6EDM}jL)g~BpsWR3>|9w^ThNIM#YHEaPYJ@!PKx$z!0G(dt zu9Y)sXk{~N9A zk2W+MjYO2?_rS_@Pq{Mki(hsP`bo2yZ+#T#GBe5QucW%X3vm~7JX?-Ex zUHv584bvpu?`x}Kt~*y1p?a;lo&UN=WCs7BQbWwbmOeR|*-UxY$6R%1dzY-kF3E{U zqw%=S7SFDZa_at^T}VAJP+2MFDHN5J`LsdQroy3KE{BPj0hpjTstj=yRzN-#L5F9u znFo=YyMO<3fNt#Hzf#@@=L$A>PdzBa2${_xFGHv_tfY{gSZnlVb0Yb}tR9uqik@kv zWhBb7*4|=dSwlTlsu1i0PnOAO$Ya4q$j&RpR$AKIjWbXuN(uDCVKfl%_npNd>RjI> z6z56AmXh`Kp0A=aSTJMrVI;>Pl&GmOFoDB-ag1rGkw|26rJ7WvG-{<>CILp7B8jBh zq{6kz2c|I^<5du{%M|>;NPp6-z9Y5ZXZcIO!ZahE~ALMzC9PwNuii?SB z6zAXZEcobLk8gUM7&*x6vfI1xAUw{ONwcTb)in@@VLUy2fVg&$cf{j4f(PMo)Uzt7 zel=44q7^8fm%}%TliGrN>JTVUch7bh6}I~{B2j+XQuuC{mgVC>@!8)#d7TmFBV*n6 z=bn3R`?K2-aI*c`M&vdg%6$LA$DExtFW*U_EOK`)H_v|l4F>r}mOFEK>^Zv|*%vm| z7oXskHfbx1o~IYB1kPm!M~ry%)>g6)%~Jlhbw)6RMC}3}gC4Ia@-bEuHBb2%tkX@~ zDSG5IekzNBVAzT21wOiPLG3~OS%r(|H!S@q+uxkeJcq>2(Z%={qfRS*o+G!ELx*0v z*eCz@_u9L!LFMsB(%|-VB-nYuCtolj0|taejC>GT`C2G>U*CzT-y~-etn?7#(%L0;bu~51 zAb!2HZ{6op#bQV-n~Y6VZ-s98*xtQ+AN#tM70b>mn`)Qs&Ws9Vpwk)%+Ilf+E zQ`4dfqeyGp_H7RiAb&vC^v`ME|C-O8-XwkTKmX%5Kl|6;w+2~88SDHzul#R!`g;@) zETa0AYg02n8J}d+rY485?S*LlL5*@QiS|=Il-Hh}&dZ1WL&ctfMTX60NaY+BE8)`^ z#8R#~U&bB>PX^N**;|zX{@~H(VbV9XWLbYB?jaXsU zBCMTjZuB5Jb)l=Iw790G+=t5T^BftEMTj-pLg+JP&J-=-WA9pkYhCNs0#o7*;?>Oe zuQBo$iG$EI$-lnk1C*J_>aH^qxEC7$w9R)>H5^9J(%&0l=Kg#3JU?sGmxn_Qd#1BW1>?GZd5!(t%-eL>`+ z_j10bT&TkDtYeLa)Q~-ueJ_OUYqml5?A4$! zWv#LA#`fNR@B0tDpZodTb3f;K&U3cs+~@m!PQ+bZO=kFMH~;|5TDLIw0007hLI4IB zSYSO1?7;$Nb4L>cocw#Tn(`9>fPYpCqh{!rwmkVX3HvZ;cYSyF%hF>X^&8L}Hbzm1 zVcfZvcMM|8Hd4eK!`_!Y1IUouT(0Rd6<954G4!V%HWL)j-2a^vD_nJ#2Pu!P(E~v{dT-n{n3CVQw z*+$9!?@KhLj^!@z!X72je zhFIoOmD}Zu4J;3Hgs;&JRUVprR^^?;hF!tKTgD9*&JR6>)kh+aGWyv@E=|#uIq#4A zQ5UinpoIDuPW2yZt2dk6WThJ3e%GKWp-HB`w1jy3D#!$HUeK?H`DIcR88;-#Ub@yq z>IEH}lB~6ZKIa3?`YGsKEc6q@K@ICDOV}!6qxMUW>M|;d(Rk}rmTw2fn`}nDLOd>+ zdDpFrF@AQ){pfjgNqdqzrUB?p7t)yH3Hwlz`_4qQEG;kOeHD*y9gA%!cR#f5i0Vsr zNX(tZYRZ-d@@-YlqK-RoY@BJ1-1x#F^WKo33<8U&n0=a;Ly?K5pO;+Z>sf3&rBHNt zANYv=oqQ=XisP!(lMWrYKw6J2d6rbZj->~(^0)Zc&eo)>%CkWCug{gZ=tmDEhJQ#o zuzXN&2%s#wWUg|2CkJjiSstcP#PR}G;8~^8?J>-j>ibcZOh_@-+$p*$=_xWQ{6kDC z)TBHoqUWecP0G6>i1USxU)AM&_fiTX&vRQE-~2n6Dds?`yZDpakFZDd=N3lao?IZQ zX+QjH4%Awnu)L^L1$2ukJ~}E*FjU_PZ#aR1{ zgAa)-E3g-p)2%+oReonshqeK4^?U_CN0nTslL@Bi5R-A^ZzEocho9K?l7xPv!u?%- z@uJfxQR99boQmvXO~LfvyV*BFdNmlj^S-Yf(w=aI2X*>5yYB zG27InSH8dfCrb>HROp^ElzEQJRZJzI4al~sdj`j$J8cTsE*X)~Oo(06`>_GHsh4XH zmAJxVtU&J=KM0AW5gZlm7j1MUP}uA-Wpr7|x;bT?_z1-#0hRQcsz6#=W1tA7$`W8? zHIpoQhHz^b$H4iu7H4VsqIH|~nE+?EACmb6|My+l(ivHj+Q`Kuej=IL2M+drFWH~e zz!L9coB7X;hbrdyiD#)Y-!_jplN)ALv0b-{$_IfIh0JHmhj~W)ZW$B?C;1&Hn-eR7 zpe6Z`C8*;jNp{59m98vc+>U%`m6P-u71uiNg(ZLux$)Axw!TNnvIeOdsLM4_w(>;Z z6rg+GC$Q{HH81gRN$y=;W|>H>(woYq-*Dk_g-yVdCZJ)NM0@l23ocZyH|AHR1MSDNy+Vs*BX?}k>^2O~nj^1xf z+En>dJWJk!NG_GWfg6n+?iCcV`oDoEl9Tp`q!nWudyjBzc3AJltZ7`@OL2!R z=L>Hs1vM>8o_`dGze13Vk!y4l@IOI+i}NYUm{r=}8=f({agxe&D2$n*D$SyWY<%6) zp}RuMS(ABGn8EVC5Q3^fl9kBwOau4hUJ%6ZM_yQ*2-BG!y9sfgAd*^|OIo?WFH4KA zf}!jZns-{;bYzDq{u?o5@xJ7sek4iuTy`~%?-vTRk-NUP`-bgm8tqp_<%4OxPU!{~ zc5yHGZq8vUg^#<8A1!#_5|Ho^!~aOGw5z1B4T}sv)h{xEd^Nc20GxBdYKDuB?~9-< zy2j`w@u{J^aa$hzsxLIFWX&+!V3LD3;RuQhZBjFcV8L!}-6E_g|R+_HyTjuj8-a z9@1H9zdT3*lNFj zWehl90c<7SG(y>w=&k;%=uG-&?x{k#~6(kd6_!OP~|(7FhuLih}_MWq;Q=wyt* z!1HRnUiElQkuQVcaz?gxF2&?(54Dv1(9-IlNcwV1-6Srppd5Ou(q?$3;H}8o5hvTV zvvYoAT5qz|150*ylIU>cpM(2Ph(e*RS6(YwLi`~ngx+?)V-ccAqx^mkrbWhw3+!~$ zi27I=-zxa@rC)wlfU>(lvn~*EY}OhQ7eU03C&M$kR?Kgj)huOUsdS5iOo6^K(k%lI zdjoFUK*pHJ(LSqN61=i?UOcx%4$R03L8n7T(YhkJed4~?Sv+|QU7S|yrg3#L=Su-s z6`=0XA2m69sT*@QV7?iQugY8AICskuWy86|5`&|=ciE1lyx<;wmG|z0%jUEbJwkiB zUSP}Yb%b~_iLiXja8t~`_dWgHOYxG$I=vGwdm5Mu#1Bg-rq?q(KcGbhf%JwLoX(oA z=Ip_i&ykXf1@C$5<%n5sbjkpv*kl+}Pr3cslGPiUV4-=K7EaRc6b#qcg;hRQ&^av}Y_r0$dW^<5zx!vd3 zk?oi(Uh~hNFQdj$-zBM#0w7Ncm9fu)|;jj5zkByCd1?N!zZM;?^(^%!< z^6Q3X)(SY^Dk_HmTf`pqPQdMJBsGcnMkwOg!G3q>hriVC6ZqB>lD>8a60tR8{?~R+ zCjm>~*MXiwsJTFQyn`9v2m$mBF*eRwSd7maq#4(4NYbCW4a$Xk-?ymEMwZwnmA|s( zI~@==HK6L5Zs!Ji@p>j?>-90qo+L?D+bS<><3!vyV;A`G4fgdc8#lEs;{EP%8~Fy&&E5t_`_4Y<-&P8v* zj<6E`Or<}U1plHKOduF_Hxk^i)LpRsYrnAA33||TVb@!<9Qggd)=Pfv8rc72dN@;} zI8XY@Gq8`nFF?Rsp~&77oM3z1xu=~`Zz&eYGN}9e(mJ8vyO2vIxqJkSl9;c>yz?fg zzG%P=OErdSg7eQoxBPXs2odE$Z#e)>?~6iIeFfVwH$@61O-%)iSD7!A0y)iR(f;G{ zh(0hf#(Qy%&%02XhC!d0_3z^|eKC=wAUR}ENU^rwrpu}j2@`0I*a!eE8HAhy5lZ~G z@F9!Aj+-t?kVTi)E(Sw6hpUC5o-`hcn$}n)ApJ3o)-YQNbUXr)2$e@#n7ma-cfAP+xK_>jmjnzf@oO8Eyfdtg`^5H+J4 zs@V2K$e+?kVYu6rb2gE#NE(ReRqtl6iq*8gRP#nb2YM6=HuRa&RqtCWQ@Gc6AAjMW zSj)~AJBOkL#?!q*J_!sazZ=%Dwq!481}c)fZgjK1kTh+&2runTnL*(r#OIn`-^ngP z7zRPGjgBH^SNFt1Qo~j!2bwf|OI0PMl{mJJ25Q`X4<;0;3pW4Ni)!TAk09rU{T7gy zpI&mMNR<0s3yY9>_`RD~OVPQ>2`NQ@0EwgKW@fw3c@622IWRW@-HU6|Z`fR+ z;mf{geREUgXYi5I-^;WioprNQM=D@}b;h#~Sai z?t-9$)U346y$?s*`08rzFoD1TmZ|{Z42?EWNY#I=ept;EeV{jQ*>rwrxS`>UM=yuGI z4%cl@v}MzU>0bEGYSR-myK#j^sCr&+$0zvo8*`d3HY3{mEX&U$)z3V|k@WYlFjTDH zRMNHt1%}kW2-`Y;)rQHTbM7&k=8nyXPMylv^@Z6?WTK@u(o3#);%+T>3}a)_x`i66 zSsuA;;D7>aD-rO!4sIg4veAb+cTY9!1pblKqcJ5iOs)2d^DK#=T@eX4t%fx~Wldda?hZtu)?dn4a;F<$MD4 zworYHWqk~UShsW{%+fdcf%enLeVHy!^&=JHDwMzPT?fMN4wm3&LCHx}TX7e(&HSv2 z=>8Qdj^FQCCX)WsfKTwcxeMCp1-4S^CsCR(Ng(g z`s1Kkj^PE<>chv%EldJSavE6Y7;-7yJ|1z=t_!g(vusFb1Z=Cfpmviu`w0kW@ z(VmrcCNO{G?TN%Wp8=$B&V8Vv-?SnF`%8I959(t-{;?lH8dm*d*HC}KKgSekm_t$H z=5y5&q2|~wtz;bpNw|NsdoFR}@`Ik)9SQXL37%#sR@2gh|JkL)9I8LajBU@4A;}cE zKRQ)Bu9pa5k-?Cq?`A(?Kea8_d3>s>vX37k@`2wdvm1xJZI>G5-6`E1t4QYG20%kK zk1tRQsrC3V^2~8gQ5mYVnneC2auytkINQ z#Fv3s@}K>JU;(=viXVG5-7l#p7VTe`eXmq1R+t$C5@t*~pZoC=&-AwhoGOEvs82`t zFY!@j_MS%ddN6clYkQ@YO+B$vX=@jY&E8;G%+(h3qi&~9y?fvZCr6v{G6OEYNBrd* zd0KKTX9gjIDy$ZqMM?`zW4ACox49q&QtJFZ5 z`Ag?3fgO9h7i)tJ$9%ToKWZ~%k#~)N&UZ;5;uW|5hl5QadFIEMDif`@-xEq9gI3`phLawZVk4<=iRg-$0b$L8t@WnS(hi$Y)Qto*h# zv0S$SOnVVBYDf~}7H*u<3u8SPRSs~IrTnvQDRr@Pn zmtI|;hWS>kid>r99SpbZIK$@iaiwYZezRV&_iHXxKMo(n5hUOJYe@hdCv~Y2&h#lp zR^pfX&zda+$Z)jw@f3!Gyg}M)DctC_2UEMTif^)q8~$;IKP!)c0T1vW7d8#MmiABT zDQJ~x$dlt-Y8aS1|IzFB`an-{KX6XRe;5}A7r0X|*9SJ^+uxC{i_s6U&T|cv>V|v- zp!u<%bnmjqn=e{em+H|tPHm9Fo4asPaAn>t^K@_;^F7iQngv7Ni!I|ghK1hreBsY? z9pNFmC#`UVUo&qa6nNwR`c8U@t>WmxoGto0;8Cx1d59BlMaS0YK~RNkHN@yYht7>K ze^3$!0ch+x;VDepL-4)lh96l3rjiAMTKe#Io%4ZIy0gYI`D<<^&%&I)jnJ%LjtnWn zBS{{GIa431q>ugM)F;yM)@(=O5+0KHg!O&i-uGvRQhRWRy|^s&HutV=VBh`ld269s zL(Ng1PQl~IEc$AGG@p^Q665r#!^HaztU1aI$}a<2RQF#CPHmfQurWK(_b^4T%Sa@& zE-0?)(d*J&V|QdYmvJV5zCK2eR;7>|ZPNRGo9GRK#v28}F*`A(^eV;av`mrii4+E; z+Sd~gzIHQ6n8uM3R#PUTJieZEY|KYv4hfqZYCQxeR$`J-+siG3)j$~x4BBJWEIkCLX}ltWQSK z+xtJe{e!`D4Bo<;2{&@G1z;wSutzI73;1uKQj)Mb7<;A;GS%fNRLH;(4 zCCyoHQ17ez8UbPiY#Uyr?oC0h@+o4-7oRJk;Z60CLN`r+fsI6%5AS7h<2k1+X|k24 zoH&-pvUuI9q3+p1>n_BuvLa|6>cBvXj!L~O2^im8VRr0#%y#)(oRuD}8KmyDMWCEO z_>xK|Pg7O|cnq1NxGjy&QO|;eL{vNY$Bz(?{djK$<7+n@+=p9{e0!+oK>#kWoFE zw&ibQfxHq;JQ1!x)K>Oa5LsY2?!t+3o^xCB1s7U$IPGv09E2RRzzcWwMc`4~j1P+kWD(0@E8idHI`6%BJC- zkRdW^K&<&z$=%J74S5-kC7&*b!XYqp&!i%!! z(X(&^mwoVr$!?DryV?bUknyk&+VXnrt2^Rjj10Igtj z1~+O&j-OaTWSZLaCiP0zTAdaCWt|McO|(ST3w}K%NsEoGwCn$;TTF%OiHxWEE#H@pZ`!$buM5~==koo0_`{xF{$pa zxL|ab!(A!Jj(RmZP}BITx(S4j(^t|h1!J$N)Qy?ge7b;&`M>Lwe-|sr8^^pnqhU)u TyJ~dc4G^HEp^GWGVHNs6X`6~@ literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/.gitignore b/apps/mobile/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/apps/mobile/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/apps/mobile/ios/Flutter/AppFrameworkInfo.plist b/apps/mobile/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/apps/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/apps/mobile/ios/Flutter/Debug.xcconfig b/apps/mobile/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/apps/mobile/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/apps/mobile/ios/Flutter/Release.xcconfig b/apps/mobile/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/apps/mobile/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/apps/mobile/ios/Runner.xcodeproj/project.pbxproj b/apps/mobile/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f0f7054 --- /dev/null +++ b/apps/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.dariuszjaniak.warp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/mobile/ios/Runner/AppDelegate.swift b/apps/mobile/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/apps/mobile/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..fdc7e5db617e259a780c8751face9f4eb4deb08a GIT binary patch literal 35861 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 Bm81Xw literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..bdceab5a65c9ed77305ecb201e7e5957cded6c03 GIT binary patch literal 900 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 UEn!EE#Q*>R07*qoM6N<$g1QUY*#H0l literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..fcfe1a3657ffe7bf5bc9eb6ef91a0281cc9e4280 GIT binary patch literal 3690 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 IM6N<$g5uCDtpET3 literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca0a7d2af7ddee31fcf42ae66cc10720eb8add6 GIT binary patch literal 1521 zcmVrM4i}R!R{H3hF?G8Rr(0VKOu(b2Ec%&TK~aVQyxb`!?go zbS`tBHjO0HWIpCpe2t)EfG9%NPApg+6>4cM&=<6~y-T^SFX)Tg!mnx4|Ly;@zutS! z|D1CRV;BYn;&8bxYkMzg)$N9nLF>e%+XclNDjCk9(aSRgyF@EX*}Rzvuz!GISVO<+ zP-pX?%^Y_Xe)y9<5y?`y^>`$1`mPYc7a-7Qxa%E`+o51V+4XgfvH zms)50Rr7!CW~HvJv5d_Z(&(_m<@&6%d7*t?VnLlBcig^M@g{s(+neZd7ZaDXz1^l! z_@Y+5&?dp0(J0||LkvA#p3W&@a_JNbfB*z}o?!Z6b~+xJp0|z;yW^pCM>hZuStp`N z-OLpoEvk?)*kN)smhmsT?w!#n32OCBynv&bTMkHyj}}${0K%tAr49OQKW@mj9&gN|aU!D`3^395X1cot|`|L1`oi zx>5Porp%?@BdeL*1Zpvpss7=Ok^Y%Z5Cpj)sIIjUNv7IR)D2;73^O|&b9RcXHC>y0 zxUi6gsn0UjXCYz|K;)6=4}e1UeHnvYgk&I|vm20OMIGIy(Kf?~8wy?q$5Cq*$~jr} zij4(GMwo_!)`?F$n;ZI73DKJAwRicd?}|5(0V#YtL2BCBb+4~w^P{Y^gwEDxuINN@ zWlWW}*KGPWib@+n%Z57r!{uqdjR1h4ZxkD%cf$EnzV4T&|GaQa64FwQ8~nr9c-cR$ ze)Uh?AU1tnq;yuOh8;#-pVw#|%NkEvN{GDSk0%p_qGH(}jA)#y!jO!!IQ; z47)I-u4`+YAU5+vTOe#MXKhiv@46e8Z;eQdRervhyH;(kympx!06>C3GMU_M(%0Pkeb!77jo}|!(8xV+?pmg) z$s%T^H(Zg14BM?u&j+#TJ7&|X1<$Jjg!9pgBL9Zo+K~{g>%$t0(?Lt3;2{8DzeKTL zqKMdVAc`&E+_lW8<9N@5LwTjKHp~k>WqBdg<&Si?l(G308G@Kv$jpyQCzHsqIE~q9iqAwAk-K{BUA=aH$DQoJ0viNBg$7q~m(H6O zj$z7f2zz^?5Xpph=g5w#+GE(lC7!MPY*<2PZWoBp5B4my%u5gowOSzdjKpI_8yZJ6 z#Mf?d;jpZ1aT3L&m6e)yYW3zFA?E-2q&y>QpSaLhw`zovXEyV}hfNFUBu+!lt=~o_ zkpjx;0ci)b2bIMft845;#ZmgSeSc<;C0Y}OCWn-D^w9+s8u z6Bpv=X%;OAf}{*qQ&r7hqt6;Ns%E|Rsb$QbfH@$UMCPZ^3X#mM{OoFhc=7*>HZT7H XXCxy+e~^*i00000NkvXXu0mjfM40U1 literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6f8e3e917e2b5ace9b593b08690645342729e2fc GIT binary patch literal 3079 zcmV+i4EXbjP) z3s6&68prRwxp_dyi$H_~AtWT>oe)&yWwotPiaMRXT)V|pYq40`b$6z9Tf3dUrk%ES zy3^Jjoz8Y`SFN4aX}4Vz#A;CsBH<+#1VtY52uOfH$b%&J?(h(m$Iav3a0B)?41}A* zcfR>^?m6dv-#I5RilTrkH0t|LXyF^?j4mn@J44T1vczET!f#S^3n zmSzNtIW)RU7GNjKX-$SNn(Gd?)Yt2EZn;W6qX^PzGIW{@XL~z7X{_?0z>h4I?UW~^ z2|}E-;8?6av+>Qk($BBfS{6tPauSo>_H}E+*R2idf{?xHH7f#w9sUWHSO|*lZ>rf- zb9T^bA=FRr;#_Y>a?zoDx%%CNbS6y0C^@HCZ3cZ#X~DVP4$pY#T_7m>Nn_>zI-3us zWTgw1&Q)x7u{1hu_L=W`%|=gp=yhB-46Z3YdLSwDnTYsV3(O+c*`6Eg&U`m)vwGe= zufvX_&t53B*zG$N2{YfFE|x~8U3cc#y{Xg$C;&WPQ4B+1u1q!Ut-Hi(Gw8F&+SKU$ zy627Ll^1iU^o`=EJ1^cA3qjGG(t>-fpPCBt*FT*2n6`~_1NZ;Q3RspcLT`<%pr z*teD)znGOng(wqOCdImGHtwl8OR8_)NXRJQ_pf%UT&A+#yp9SuI~SU;d!} zVux{0@qqSf|fLO1_B?r)bh;RlA5Cr$C*CZ4iL;%umN4#S3XpDS?njmK!5YG$D5Q+=C zTIQ#^ncx*`V_2jQg|Jb=Ay0-yE?}`XghXH$7`Bdn)>u`c>mEh0#(pXY$=JMSWlGt2 zm~AdHgTvkRlmKvv1p+{-K!j1?rrDTMbm-dfppynaZLBQ3|B3sSgw9n!=<65m$7wO> zU3ZoMaEZmEG1yeBRXeZOo_Ag;007K(`;PLH4eOu5n1x9ExC>G&hlAj0*W*8pL;asz zsnm^{JB-6t#Ezh2St$g)^#z~};ihXk|4V(PrK`pXYuSn1_GX%)04vd zjrxlK54WsE)v&zmHUt46!GFoxV6l=LC}aDxU>XH-V8Lj!Y0V?`gMG!?)-&36lih~T zWh}hqHl%>yW=YIzu_+Rk%YnmqATT3Xyen$ih|PMix$d31GOc;U6Ha=%WjTe6#XFL= zei=UqXLU@N`n*EDNgTaP^YedNe#2>PCZ6uI9Ltq0l75rEfeBNIs??v(_$oC!O%Ss4 z;z_%UtNgJcxn(^ij{0}?j^c0_qag(ZzevvVoKh3)j!rt(-c;4!Lx?q8VmVxU6UgvsH_o1y z+$N2S;_&g*?hF(N+P7kDa^WFDtl<)?*K9;k6eIVwL#E0tFPf!R5PBm%4Nuk1vob*V zU{Dw#OFD3gHDa^281&M)XGYs2;=4_Tck0TlcW3<`5{3(X{qR)n5Ig57xy()7d`M~YT%qPTVI$O!wskz=*3w(0b*Sn z9*DsF#}b7NL5|F}NO8nw&Am`~dEGXQIdBw*Prg_-6n(Ac+|l$6IPEyYfvYuFhX-bT zt6s0`HjQBPgZ?xIeg_Dclz!88B}WpIEt29lg+Z6m@W!QIF$!SsqM;L!uOns#zxmgK zi&;6?y(}IqcKhRJf9SKAJY?TVh!LxQ)ck144<#9!xJ0z}X;=~a=F)<)zAnNoy9!~? zhL;9!rWGCeI&FO%FOVpk6Z77!rN@tVT_@0{+YtF6{hPjT`H8Rg#3jERy#jw9fuQKO zZCCOuPLY21O+j$f!GhRduRXuNspij-i5tTr!~8gI7#X&X9&2y>xIuGi@FubD;lv{$ znl6)JcXi3`>Jk-Ckmw&IVR3jg26j9i0Z^mO+G@~W9_-WfYe&3tE3IeaSv3Q__@{#2 zgSR9s76iYGi7`~yt>}_Qy&fj!xw>Ic# zNsq}+z=%PDSQfi2M-oe)JEOBj8tbZ9>qJsMrdVwT0(RYyAQlP$mj`bq&Ye=-B8{!k zbsungC$f?k_|K#a>@wHp2v@iSIepeCZ5xJI6cE~SSzUVn?J+9p1e3>wi~2IzM$u{J2b*ivJOcG_ii{xOTkr`okj0Jk$k&U^ItcfqR29 znUm6IFBFzPloP@vCh>!fk|$W$Vs)7eYk&TxBx93+!Sr-pN}j-pQVt;bap%fVe{$iW z@9%pwp3wN}Ox#ZE-U<27hSv@H6(_!WFJaY-k%&rgkTp|@&Xr+SX0!yemu?x_2mgXL7qI}sqm<|4rYzz1{QSU$`W~_Sng3h z(3@Lc^itI?+au!h6>9N}TnYYe3uaJYvnLnY&og?f<)em*j~g^uBFRp<>LH_}GD6PwJTJeiM9SjA z_XmZ&RaZ)!V~Z9gR{*C5ih_NZ-6oIh3|dU22|{2Pg0@KF-uvwW+0J-194b9EP&78r zdQPtTu>K*535{;pVfF z87y8t{&66aVl8E|-&UpV(s=0^0VF~J!4K4{CtlxvHq2M3kF+#g)OC~Rm^YFqPl&%2 zWxXvH1%&>UoVEPKm)>^fC5ecc>~~eGrhdIE>JZBfIF!0><2fRCe=b~T5an>%dKUF| zz-M}h=CM%4{-liD3q|+1S|9)%Ovy^Qdq2=D@vQ9;%27L#Ur~I|>IDM8{-lge;^=AD zXAl1ER3y+KXh-=;5)arT3mQZ@n36S9DXd88c9}AQ#oJPLyp8DdguNLtlYJ;{{am~8 zu)@l#gF-K@+xk-FFP~qlUGxo%0)lz+gm+b|?(EU0;UEUjrqU0r%=}|`^j|7Z6Sark z3&{)?zpq~HFl+5p!eZ5urJAhGgd4ab4J~C)6Gm>GNLYX z%ZyW(%?WJTvP2h0`4A$CSU!~x2TDPp1*w9Ce%{@NWzb$rdo2g`*Z$GIectlR-FwgT z+*=?NMFCsXtCWTEwu=7lwqcb@r$qqUVT3>eK(-fE#PHAJ@i&IWaHuq!VUYDunWF1( zL*>nZo|%)wfBnm~vq9dYoe=l!Ln1$=8(XgvV`a?(4tXW#4L*TOy z>I&tp7gAT|1cjS_H0k<8r&&>2baznU4nr(YbedJ=zvU-o?v75L7#w#UH5y(XQ4=Tt z?2(m1B+^b{qG@E@bytn_r4pTB_uZ1{Ijp7J&~eu?MVEWms2yMw-BNY6IVacG!*k5_ za6_fLmF&?os{Z2s?Gs`NRHstTW_ogsZ`x`P#HK(+@-}yyI7V$W6m+&i75&{VO!gp= z0=#G>00hCYx8T@w?4g5Ug=Sy{M%qBa_)Avq3=qBeRLYses2v}lb}q9&4A-29}> z?bX*^(!+wwbu&NC;i;dBcLnkJEj6vqToqG+%XJGs4l|;M;c{h{^~nvT`6jWr<|AZ~ zv@tC95E13Zu`3w_7qDNk5JljOzc1ez?6VnvC@%H$S2N5<(gOmg?Fry=WqDFP$rfR^ z2YROx1pwew>s=p;N3TW=BM5?`2!P^GHPdw^o6@}*qp-c}T5U8OsJk(3F&p4ouNcM( zlsj<6)edtgo*TkqmT*IabS4$TemRVyeHwLT|KrP#nq9~Yt{qp~Y!ir%izO^is-3w) zU*=*?$VYLh=i3|i%1WIo6E3-0tzZBEz7$LL#iro=1~Le4k4Q*lax#mrJU8lb&LUdcFFHXoi~D1G_Uku(#~JU~RRbF_BY{*O^S5-!+sKcZ$C@nKG>F3SqV;Oj#JE=WKmO@a^Co_*3$6$1ZeZH&zEL&6vWTIQ zacSVq1KX>whtPZ_oM0z30U*D*=3INDQ&a4K*HsG}vq}n&izPdSiBsKanWNHaKdddg zK>q=EQg$&Z`jTx^|W!gKnyw*Hf*s)2*^hZs?9k@oPZ>3W*fYaJ;bx0szGIqV?cQ z#RP%^K#4+LqL2qteYQm;87Ebv7M70~@s{duc{A|#w(lC8$_U&UQ&ckX&phX4Qo M07*qoM6N<$g0clpVE_OC literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..678024cbfe3d3cb7809abc7d56752a1397bae47e GIT binary patch literal 3456 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 IM6N<$g5uCDtpET3 literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a1afcc78c4bb569cf2206701a2eb264675186cac GIT binary patch literal 5602 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 literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..12a2aaa62ac0269a563e49153c6dbdaa79c0c6f1 GIT binary patch literal 2312 zcmV+j3HSDiP)hzn zx}9n3^s$cPYRBER=&p|iC?Nqu010mr6Y{*B5W$!S$-T+t(QlY=@45G! z`+qt2|Nrklxfgag9E2$V01yOm-?k!-+9#UozOMSAKIQnxklAEKo}WLPAOVO^rKho? z6?}1-SdtdSb+^{rc-Y@SLuS*V*7|=c9}bvJvtoGwSP@5$$<_x{yk%LJaLhJ+gfJ*Ahw+#r_xHUDKHmEnDYC$Vv> z!;M=l3yzfwHpH>>PBjdmZAlrfmKTD@)|(d=BCBcauB%=e#Vv@AaqT`Mc0|>9c7gbn zKUf@&A6`G(xVD@{rabepL9=P^{i}0rSQwx+>i68e{8{1JXFhhQrG7p~c2qdt*}OAZ zwt^Q6TM@^n9S`T)o^axKO*j8jyb-Q>qM0Y!B3!uG+u3CpgD>@U&9xTrD#w?*0km=UuIbTI;)w`j9OTfQKUk@DL^kW0J^+a*DT4W_k}Y{vf0=_N3;# zDarbE7D(~1ppj=;UqU46Q5`#ZELK6PNi_;&p_n9L3esEK_jEm+B4G(+hc<$aBsLjTm!lT9kEwp5`DHG zQ8j(iTJJN!0s=^6vi8eXD+FR+118K1Vp(jSD*(8Mgmc=CH*Q~?q1Tg%r{AkN)wcc( zF@x#e4gip`1^)Ym1a!oBR62qn9$}!Ww=-ZzqusWvu8K}14OvVMgy29B2LW*)$S7`A z1v+9bmE-}l!yZiE3q5L7*9sA+!@W0rvYm6FCg$y9LDGT&loH8_rALq;qR$hYTKTC{ zD?12bP$Z~{dHYyEX1N^UyJ>mrVw2ocg}>b0KVdxexb0v|ohCG`9F|+rFCt0gKj)Ra zo{%2ARu)j{@1*AJSd#VIrW*&E>oCtAjy@JeqFgH3l*Rau|sD z1QCF*iZ5`4@ZNRdEhK`ui!63*ZctF6%jD3*4yhsp-LHK*@zL%C? z*{ikH#&DX}1S^KkaC=P1USFTD+7_44YQY5dB%IJuFrhfFHw15PFw@c7r65Urt zGlL^_8ph6RRnq}oXEv^>In}y;M;s&4qgNK2hssXW#MFkde<&Zkm69EL1q}fBpy8@V znljpLSNb(u6Qv%#xD*<`#GFe553ZGn7AG^=Xghk~w5I)(=D*(UX1fnb4Fd@6_^64E zTPzdm2}svJ|9c%rlUt-5dYQ;`NI zU{oGT{YZZ0@vVu{?MpJ3M~jGVw~D`1Yqr}j_NxEXb~l)8>lt#;7e?46l^sEjs+x|f zngmpO9#<4Yk01kx007|y;qP>Plij8^j5ZAG#;oQb2los)=w(?Lv||5+@qCXe^l`!q zyhQMv0~cb8=u8MW-Ol+501-d}DqU+ds12h5`$FCoIsh071YC$Ut}ACvsw(%Sd`$hQ zJWz;-09e6`_4YB{gb}R`xDaa@9dy~}j@J{??+o_2Yh@2`$kYSbMX8Y-??X!cFj^aM zAy(Pj=}n9XIkoa9xy2^CZE`-GAFi1}CjYn;D&!}wU(;#P+JFnOFCMr3CTpcn6-0rE zl*v`98Dyk4sD2i?HEeUD6lI3|^5roypU;xrt(f84;4&W}S>M?* zdaH#FleQ;ixWEw1E3+5*=7it#QW*X;u(?8 ziA9pgU#wccy87gNOV+56%MtFIoURunRX$hrw`HYxFR9}PA(c^~C?id&uoqslDo@I= z*zIrEU$QNIDeJw6|gILlqqC2ncxUSQ-jt%L|Myq3PUKfS|V3Vl{5iB`}uMoto iiAi9QsUGLJ8~;BzOr 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 am;MhxeRLNsiZk~B0000 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 Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Runner/Base.lproj/Main.storyboard b/apps/mobile/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/apps/mobile/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Runner/Info.plist b/apps/mobile/ios/Runner/Info.plist new file mode 100644 index 0000000..143db41 --- /dev/null +++ b/apps/mobile/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Warp + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + warp + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/apps/mobile/ios/Runner/Runner-Bridging-Header.h b/apps/mobile/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/apps/mobile/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/apps/mobile/ios/Runner/SceneDelegate.swift b/apps/mobile/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/apps/mobile/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/apps/mobile/ios/RunnerTests/RunnerTests.swift b/apps/mobile/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/apps/mobile/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/mobile/lib/app.dart b/apps/mobile/lib/app.dart new file mode 100644 index 0000000..c8b649d --- /dev/null +++ b/apps/mobile/lib/app.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/encryption.dart'; +import 'package:matrix/matrix.dart'; + +import 'matrix/notifications.dart'; +import 'screens/home_screen.dart'; +import 'screens/login_screen.dart'; +import 'screens/room_screen.dart'; +import 'state.dart'; +import 'theme.dart'; +import 'widgets/uia.dart'; +import 'widgets/verification_sheet.dart'; + +final navigatorKey = GlobalKey(); + +class WarpApp extends ConsumerWidget { + const WarpApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final loginState = ref.watch(loginStateProvider); + final signedIn = loginState.value == LoginState.loggedIn; + // Signing in from the sign-up screen, or out from settings, leaves those + // screens on top of the new home: close them. + ref.listen(loginStateProvider, (prev, next) { + if (prev?.value != next.value) { + navigatorKey.currentState?.popUntil((route) => route.isFirst); + } + }); + return MaterialApp( + title: 'Warp', + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + theme: warpTheme(Brightness.light), + darkTheme: warpTheme(Brightness.dark), + themeMode: ref.watch(themeModeProvider), + home: loginState.isLoading + ? const Scaffold() + : signedIn + ? const _SignedIn(key: ValueKey('signed-in')) + : const LoginScreen(key: ValueKey('signed-out')), + ); + } +} + +/// Everything that runs only while signed in: the notification listener, +/// prompts the SDK raises (a password for an account change, an incoming +/// verification request) and opening rooms from notifications. +class _SignedIn extends ConsumerStatefulWidget { + const _SignedIn({super.key}); + + @override + ConsumerState<_SignedIn> createState() => _SignedInState(); +} + +class _SignedInState extends ConsumerState<_SignedIn> + with WidgetsBindingObserver { + final _subs = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + final client = ref.read(clientProvider); + _subs + ..add(Notifications.watch(client)) + ..add(Notifications.opened.listen(_openRoom)) + ..add(client.onUiaRequest.stream.listen(_onUia)) + ..add(client.onKeyVerificationRequest.stream.listen(_onVerification)); + Notifications.launchRoomId().then((roomId) { + if (roomId != null) _openRoom(roomId); + }); + Notifications.requestPermission(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + for (final sub in _subs) { + sub.cancel(); + } + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + // The sync loop keeps running in the background for as long as Android + // lets the process live; pushes cover the time after that. + Notifications.foreground = state == AppLifecycleState.resumed; + } + + void _openRoom(String roomId) { + final nav = navigatorKey.currentState; + if (nav == null) return; + nav.popUntil((route) => route.isFirst); + nav.push(RoomScreen.route(roomId)); + } + + void _onUia(UiaRequest request) { + final context = navigatorKey.currentContext; + if (context == null) return request.cancel(); + handleUia(context, ref.read(clientProvider), request); + } + + void _onVerification(KeyVerification request) { + final context = navigatorKey.currentContext; + if (context == null) return; + showVerificationSheet(context, request); + } + + @override + Widget build(BuildContext context) => const HomeScreen(); +} diff --git a/apps/mobile/lib/main.dart b/apps/mobile/lib/main.dart new file mode 100644 index 0000000..ceb46dc --- /dev/null +++ b/apps/mobile/lib/main.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'app.dart'; +import 'matrix/client.dart'; +import 'matrix/notifications.dart'; +import 'matrix/push.dart'; +import 'state.dart'; + +Future main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + + // A push starts the app with this argument when it isn't running: restore + // the session, show the notification, and don't start the UI. + final background = args.contains('--unifiedpush-bg'); + + await Notifications.init(); + final client = await createClient(startSync: !background); + await Push.init(client); + if (background) return; + + runApp( + ProviderScope( + overrides: [clientProvider.overrideWithValue(client)], + child: const WarpApp(), + ), + ); +} diff --git a/apps/mobile/lib/matrix/client.dart b/apps/mobile/lib/matrix/client.dart new file mode 100644 index 0000000..c5b8134 --- /dev/null +++ b/apps/mobile/lib/matrix/client.dart @@ -0,0 +1,183 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; +import 'package:matrix/encryption.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart' as sqflite; + +/// The name the session's device gets on the homeserver. +const deviceDisplayName = 'Warp Android'; + +const _secureStorage = FlutterSecureStorage(); + +/// Creates the one [Client] the app uses and restores its session, if any. +/// +/// Sync state, rooms and the Olm/Megolm keys live in a SQLite database in the +/// app's private storage. The SDK also writes its credentials (the access +/// token and the cached secrets) to [FlutterSecureStorage], the Android +/// Keystore, and reads them from there first. +/// +/// [startSync] is false when the app was started in the background to show a +/// push notification: that only needs the session, not a sync loop. +Future createClient({bool startSync = true}) async { + await vod.init(); + + final dir = await getApplicationSupportDirectory(); + // The SDK's file store (attachments being sent, downloaded media) writes + // into this directory but doesn't create it. + final media = await Directory(p.join(dir.path, 'media')).create(); + final database = await MatrixSdkDatabase.init( + 'warp', + database: await sqflite.openDatabase(p.join(dir.path, 'warp.sqlite')), + fileStorageLocation: media.uri, + deleteFilesAfterDuration: const Duration(days: 30), + secureStorage: ( + read: (key) => _secureStorage.read(key: key), + write: (key, value) => _secureStorage.write(key: key, value: value), + delete: (key) => _secureStorage.delete(key: key), + ), + ); + + final client = Client( + 'Warp', + database: database, + verificationMethods: { + KeyVerificationMethod.emoji, + KeyVerificationMethod.numbers, + }, + supportedLoginTypes: {AuthenticationTypes.password}, + nativeImplementations: NativeImplementationsIsolate( + compute, + vodozemacInit: () => vod.init(), + ), + logLevel: kReleaseMode ? Level.warning : Level.info, + importantStateEvents: {'im.ponies.room_emotes'}, + // Refresh tokens: a soft logout (an expired access token) is answered by + // refreshing it rather than by signing the user out. + onSoftLogout: (client) => client.refreshAccessToken(), + ); + client.backgroundSync = startSync; + await client.init( + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: startSync, + ); + return client; +} + +/// What the user typed as their server, resolved to a homeserver. +/// +/// Accepts a server name (`example.org`, looked up through +/// `/.well-known/matrix/client`), a homeserver address +/// (`https://matrix.example.org`) or, for development, a plain HTTP one +/// (`http://10.0.2.2:8008`, which the emulator sees as the host machine). +/// A bare `host:port` on a private address is tried over HTTPS first, then +/// over HTTP. +Future resolveHomeserver(Client client, String input) async { + final text = input.trim(); + if (text.isEmpty) throw const FormatException('Enter a server.'); + + final candidates = []; + if (text.startsWith('http://') || text.startsWith('https://')) { + final uri = Uri.tryParse(text); + if (uri == null || uri.host.isEmpty) { + throw const FormatException("That isn't a server address."); + } + candidates.add(uri); + } else { + final https = Uri.tryParse('https://$text'); + if (https == null || https.host.isEmpty) { + throw const FormatException("That isn't a server name."); + } + candidates.add(https); + if (_isLocal(https.host)) candidates.add(https.replace(scheme: 'http')); + } + + Object? lastError; + for (final uri in candidates) { + try { + await client.checkHomeserver( + uri, + checkWellKnown: uri.scheme == 'https' && !uri.hasPort, + fetchAuthMetadata: false, + ); + return; + } catch (e) { + lastError = e; + } + } + throw HomeserverException(text, lastError); +} + +bool _isLocal(String host) => + host == 'localhost' || + host.endsWith('.localhost') || + host.endsWith('.local') || + host.startsWith('10.') || + host.startsWith('192.168.') || + host.startsWith('127.') || + RegExp(r'^172\.(1[6-9]|2\d|3[01])\.').hasMatch(host); + +class HomeserverException implements Exception { + HomeserverException(this.input, this.cause); + final String input; + final Object? cause; + + @override + String toString() { + final cause = this.cause; + if (cause is BadServerLoginTypesException) { + return "$input doesn't offer sign-in with a password."; + } + return "Couldn't reach a Matrix server at $input."; + } +} + +/// The server part of a user ID typed in full (`@alice:example.org`), so +/// signing in with one doesn't need the server typed separately. +String? serverOfUserId(String input) { + final text = input.trim(); + if (!text.startsWith('@')) return null; + final i = text.indexOf(':'); + if (i < 0 || i == text.length - 1) return null; + return text.substring(i + 1); +} + +/// A human sentence for an error from the SDK or the homeserver. +String describeError(Object error) { + if (error is MatrixException) { + switch (error.error) { + case MatrixError.M_FORBIDDEN: + return error.errorMessage.isNotEmpty + ? error.errorMessage + : "You aren't allowed to do that."; + case MatrixError.M_USER_IN_USE: + return 'That username is taken.'; + case MatrixError.M_INVALID_USERNAME: + return "That username isn't allowed."; + case MatrixError.M_LIMIT_EXCEEDED: + return 'Too many attempts. Wait a moment and try again.'; + case MatrixError.M_NOT_FOUND: + return error.errorMessage.isNotEmpty + ? error.errorMessage + : "That doesn't exist."; + default: + return error.errorMessage.isNotEmpty + ? error.errorMessage + : error.errcode; + } + } + if (error is HomeserverException || error is FormatException) { + return error is FormatException ? error.message : error.toString(); + } + final text = error.toString(); + if (text.contains('SocketException') || + text.contains('ClientException') || + text.contains('TimeoutException')) { + return "Couldn't reach the server. Check your connection."; + } + return text.replaceFirst('Exception: ', ''); +} diff --git a/apps/mobile/lib/matrix/encryption.dart b/apps/mobile/lib/matrix/encryption.dart new file mode 100644 index 0000000..816a43a --- /dev/null +++ b/apps/mobile/lib/matrix/encryption.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:matrix/encryption.dart'; +import 'package:matrix/matrix.dart'; + +/// Sets up encryption for the account in one step, like the web client: +/// a new secret storage key, new cross-signing keys, and a new key backup, +/// all behind one recovery key, which is returned to be shown once. +/// +/// It always starts over: whatever secret storage, cross-signing keys or +/// backup the account had are replaced. That is what "set up" means for an +/// account without cross-signing, and what "reset" means for one that lost +/// its recovery key. +/// +/// Uploading cross-signing keys needs the account password; the SDK asks +/// for it through [Client.onUiaRequest], which the app answers. +Future setUpEncryption(Encryption encryption) { + final done = Completer(); + BootstrapState? handled; + + Future step(Bootstrap b) async { + // onUpdate fires once per state, but guard anyway: answering a state + // twice throws. + if (b.state == handled) return; + handled = b.state; + switch (b.state) { + case BootstrapState.loading: + break; + case BootstrapState.askWipeSsss: + b.wipeSsss(true); + case BootstrapState.askUseExistingSsss: + b.useExistingSsss(false); + case BootstrapState.askBadSsss: + b.ignoreBadSecrets(true); + case BootstrapState.askUnlockSsss: + b.unlockedSsss(); + case BootstrapState.askNewSsss: + await b.newSsss(); + case BootstrapState.openExistingSsss: + await b.openExistingSsss(); + case BootstrapState.askWipeCrossSigning: + await b.wipeCrossSigning(true); + case BootstrapState.askSetupCrossSigning: + await b.askSetupCrossSigning( + setupMasterKey: true, + setupSelfSigningKey: true, + setupUserSigningKey: true, + ); + case BootstrapState.askWipeOnlineKeyBackup: + b.wipeOnlineKeyBackup(true); + case BootstrapState.askSetupOnlineKeyBackup: + await b.askSetupOnlineKeyBackup(true); + case BootstrapState.done: + final key = b.newSsssKey?.recoveryKey; + if (key == null) { + done.completeError(Exception('No recovery key was made.')); + } else { + done.complete(key); + } + case BootstrapState.error: + done.completeError( + b.errorResult?.error ?? Exception('Setting up encryption failed.'), + ); + } + } + + encryption.bootstrap( + onUpdate: (b) => step(b).catchError((Object e, StackTrace s) { + Logs().e('Encryption setup failed', e, s); + if (!done.isCompleted) done.completeError(e, s); + }), + ); + return done.future; +} + +/// Verifies this session with the recovery key (or passphrase): unlocks +/// secret storage, which caches the cross-signing and backup keys and signs +/// this session, then restores the room keys from the backup so older +/// messages can be read. +Future verifyWithRecoveryKey(Encryption encryption, String input) async { + final key = encryption.ssss.open(); + await key.unlock(keyOrPassphrase: input.trim()); + await key.maybeCacheAll(); + if (!encryption.crossSigning.isSelfSigned) { + await encryption.crossSigning.selfSign(openSsss: key); + } + if (encryption.keyManager.enabled) { + // Best effort: the session is verified whether or not this finishes. + unawaited( + encryption.keyManager.loadAllKeys().catchError( + (Object e) => Logs().w('Restoring keys from the backup failed', e), + ), + ); + } +} diff --git a/apps/mobile/lib/matrix/media.dart b/apps/mobile/lib/matrix/media.dart new file mode 100644 index 0000000..5e6c860 --- /dev/null +++ b/apps/mobile/lib/matrix/media.dart @@ -0,0 +1,87 @@ +import 'dart:typed_data'; + +import 'package:matrix/matrix.dart'; + +/// Loads media for display: avatars by their `mxc://` URI, attachments by +/// their event. +/// +/// Media is authenticated (Matrix 1.11), so it can't be handed to an image +/// widget as a URL: it is fetched with the access token here. Attachments go +/// through the SDK, which decrypts files from encrypted rooms and keeps them +/// in its file store; avatars are cached in that store too. A small +/// in-memory cache in front keeps scrolling from refetching. +class Media { + Media._(); + + static const _maxEntries = 300; + static final _cache = >{}; + + static Future _remember( + String key, + Future Function() load, + ) { + final hit = _cache.remove(key); + if (hit != null) { + _cache[key] = hit; + return hit; + } + final future = load().catchError((Object e) { + Logs().v('Could not load $key', e); + _cache.remove(key); + return null; + }); + _cache[key] = future; + while (_cache.length > _maxEntries) { + _cache.remove(_cache.keys.first); + } + return future; + } + + /// A square thumbnail of [mxc], [size] logical pixels (fetched at 2x). + static Future thumbnail(Client client, Uri mxc, int size) { + final px = size * 2 < 96 ? 96 : size * 2; + return _remember('$mxc@$px', () async { + final stored = await client.database.getFile(_storeKey(mxc, px)); + if (stored != null) return stored; + final uri = await mxc.getThumbnailUri( + client, + width: px, + height: px, + method: ThumbnailMethod.crop, + ); + final response = await client.httpClient.get( + uri, + headers: {'authorization': 'Bearer ${client.accessToken}'}, + ); + if (response.statusCode != 200) return null; + final bytes = response.bodyBytes; + await client.database.storeFile( + _storeKey(mxc, px), + bytes, + DateTime.now().millisecondsSinceEpoch, + ); + return bytes; + }); + } + + static Uri _storeKey(Uri mxc, int px) => + Uri(scheme: 'cache', host: 'avatar', path: '${mxc.host}${mxc.path}@$px'); + + /// The attachment of [event], or its thumbnail when it has one and + /// [thumbnail] is set. Decrypted when the room is encrypted. + static Future attachment(Event event, {bool thumbnail = false}) { + final useThumb = thumbnail && event.hasThumbnail; + return _remember( + '${event.eventId}${useThumb ? '#thumb' : ''}', + () async => (await event.downloadAndDecryptAttachment( + getThumbnail: useThumb, + )).bytes, + ); + } + + /// Forgets an event's cached attachment, for a retry after a failure. + static void evict(Event event) { + _cache.remove(event.eventId); + _cache.remove('${event.eventId}#thumb'); + } +} diff --git a/apps/mobile/lib/matrix/notifications.dart b/apps/mobile/lib/matrix/notifications.dart new file mode 100644 index 0000000..1c6def3 --- /dev/null +++ b/apps/mobile/lib/matrix/notifications.dart @@ -0,0 +1,157 @@ +import 'dart:async'; +import 'dart:ui' show Color; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:matrix/matrix.dart'; + +/// System notifications for new messages: one per room, replaced as more +/// messages arrive, opening the room when tapped. +/// +/// They come from two places. While the app runs, from the SDK's +/// [Client.onNotification] (events the user's push rules say notify). While +/// it doesn't, from a push ([Push]), which starts the app in the background +/// just long enough to fetch the event and show it. +class Notifications { + Notifications._(); + + static final _plugin = FlutterLocalNotificationsPlugin(); + static final _opened = StreamController.broadcast(); + static bool _ready = false; + + /// Room IDs of notifications the user tapped while the app was running. + static Stream get opened => _opened.stream; + + /// The room the user is looking at, whose messages don't notify. + static String? activeRoomId; + + /// Whether the app is in the foreground. Messages in rooms other than + /// [activeRoomId] still notify then. + static bool foreground = true; + + static const _channel = AndroidNotificationChannel( + 'messages', + 'Messages', + description: 'New messages and invites', + importance: Importance.high, + ); + + static Future init() async { + if (_ready) return; + await _plugin.initialize( + settings: const InitializationSettings( + android: AndroidInitializationSettings('ic_notification'), + ), + onDidReceiveNotificationResponse: (response) { + final roomId = response.payload; + if (roomId != null && roomId.isNotEmpty) _opened.add(roomId); + }, + ); + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.createNotificationChannel(_channel); + _ready = true; + } + + /// The room of the notification that launched the app, if one did. + static Future launchRoomId() async { + final details = await _plugin.getNotificationAppLaunchDetails(); + if (details?.didNotificationLaunchApp != true) return null; + return details?.notificationResponse?.payload; + } + + /// Asks for the Android 13+ notification permission. Returns whether + /// notifications may be shown. + static Future requestPermission() async => + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.requestNotificationsPermission() ?? + true; + + /// Shows [event] unless the user is looking at its room. + static Future showEvent(Event event) async { + final room = event.room; + if (foreground && room.id == activeRoomId) return; + final i18n = const MatrixDefaultLocalizations(); + final body = room.membership == Membership.invite + ? '${event.senderFromMemoryOrFallback.calcDisplayname()} invited you' + : event.calcLocalizedBodyFallback( + i18n, + withSenderNamePrefix: !room.isDirectChat, + hideReply: true, + plaintextBody: false, + removeMarkdown: true, + ); + await show( + roomId: room.id, + title: room.getLocalizedDisplayname(i18n), + body: body, + timestamp: event.originServerTs, + ); + } + + static Future show({ + required String roomId, + required String title, + required String body, + DateTime? timestamp, + }) async { + if (!_ready) await init(); + try { + await _plugin.show( + id: _idFor(roomId), + title: title, + body: body, + payload: roomId, + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + _channel.id, + _channel.name, + channelDescription: _channel.description, + importance: Importance.high, + priority: Priority.high, + category: AndroidNotificationCategory.message, + when: timestamp?.millisecondsSinceEpoch, + showWhen: timestamp != null, + color: const Color(0xFF00A389), + groupKey: 'messages', + ), + ), + ); + } catch (e, s) { + Logs().w('Could not show a notification', e, s); + } + } + + /// Removes the room's notification, when it has been read or opened. + static Future clearRoom(String roomId) async { + if (!_ready) return; + await _plugin.cancel(id: _idFor(roomId)); + } + + static Future clearAll() async { + if (!_ready) return; + await _plugin.cancelAll(); + } + + // Notification IDs are 32-bit ints; a room's is a stable hash of its ID so + // a new message replaces the room's previous notification. + static int _idFor(String roomId) { + var h = 0x811c9dc5; + for (final unit in roomId.codeUnits) { + h = ((h ^ unit) * 0x01000193) & 0x7fffffff; + } + return h; + } + + /// Listens to [client] for events that notify while the app is running. + static StreamSubscription watch(Client client) => + client.onNotification.stream.listen((event) { + if (kDebugMode) debugPrint('notify ${event.eventId}'); + showEvent(event); + }); +} diff --git a/apps/mobile/lib/matrix/push.dart b/apps/mobile/lib/matrix/push.dart new file mode 100644 index 0000000..2a3354e --- /dev/null +++ b/apps/mobile/lib/matrix/push.dart @@ -0,0 +1,206 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:matrix/matrix.dart'; +import 'package:unifiedpush/unifiedpush.dart'; + +import 'notifications.dart'; + +/// Push notifications through UnifiedPush. +/// +/// The user picks a distributor app (ntfy, or another UnifiedPush +/// distributor) which gives this app an endpoint URL. The endpoint becomes a +/// Matrix pusher: the homeserver posts each notification to the +/// distributor's Matrix gateway, the distributor wakes the app, and the app +/// fetches the event (decrypting it if it can) and shows it. +/// +/// Pushes are `event_id_only`, so neither the gateway nor the distributor +/// sees message content, not even in unencrypted rooms. +/// +/// No Google services are involved, so this works without a Firebase +/// project; the cost is that the user needs a distributor installed. +class Push { + Push._(); + + static const appId = 'io.github.dariuszjaniak.warp'; + static const _endpointKey = 'warp.push.endpoint'; + static const _storage = FlutterSecureStorage(); + + /// Where a push goes when the distributor's server has no Matrix gateway + /// of its own: the UnifiedPush project's public one, which forwards to any + /// endpoint. + static const fallbackGateway = + 'https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify'; + + static Client? _client; + + /// Sets up the push receiver. Called on every start, in the foreground and + /// when a push starts the app in the background. + static Future init(Client client) async { + _client = client; + final registered = await UnifiedPush.initialize( + onNewEndpoint: (endpoint, _) => _onNewEndpoint(endpoint.url), + onUnregistered: (_) => _onUnregistered(), + onRegistrationFailed: (reason, _) => + Logs().w('UnifiedPush registration failed: $reason'), + onMessage: (message, _) => _onMessage(message), + ); + // The connector wants a register() on every start to keep its + // registration with the distributor alive. + if (registered && client.isLogged()) { + await UnifiedPush.register(features: const []); + } + } + + /// The installed distributors, by package name. + static Future> distributors() => UnifiedPush.getDistributors(); + + static Future currentDistributor() => UnifiedPush.getDistributor(); + + /// Turns push on with [distributor], or the default one when null. Returns + /// false when there is no distributor to use and the user has to pick one. + static Future enable({String? distributor}) async { + if (distributor != null) { + await UnifiedPush.saveDistributor(distributor); + } else if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) { + return false; + } + await UnifiedPush.register(features: const []); + return true; + } + + /// Turns push off: unregisters from the distributor and removes the + /// pusher from the homeserver. + static Future disable() async { + await UnifiedPush.unregister(); + await _onUnregistered(); + } + + /// Removes this device's pusher, when signing out. The homeserver also + /// removes it when the session ends, so this is best effort. + static Future forget() async { + try { + await UnifiedPush.unregister(); + } catch (_) {} + await _storage.delete(key: _endpointKey); + } + + static Future _onNewEndpoint(String endpoint) async { + final client = _client; + if (client == null || !client.isLogged()) return; + final previous = await _storage.read(key: _endpointKey); + if (previous != null && previous != endpoint) { + await _deletePusher(client, previous); + } + final gateway = await _gatewayFor(endpoint); + await client.postPusher( + Pusher( + appId: appId, + pushkey: endpoint, + appDisplayName: 'Warp', + deviceDisplayName: client.deviceName ?? 'Warp Android', + kind: 'http', + lang: 'en', + data: PusherData(url: Uri.parse(gateway), format: 'event_id_only'), + ), + append: false, + ); + await _storage.write(key: _endpointKey, value: endpoint); + Logs().i('Pusher registered with gateway $gateway'); + } + + static Future _onUnregistered() async { + final client = _client; + final endpoint = await _storage.read(key: _endpointKey); + if (client != null && endpoint != null && client.isLogged()) { + await _deletePusher(client, endpoint); + } + await _storage.delete(key: _endpointKey); + } + + static Future _deletePusher(Client client, String endpoint) async { + try { + await client.deletePusher(PusherId(appId: appId, pushkey: endpoint)); + } catch (e) { + Logs().w('Could not remove the old pusher', e); + } + } + + /// The Matrix gateway for [endpoint]. UnifiedPush's discovery: a server + /// that has one answers `GET /_matrix/push/v1/notify` with + /// `{"unifiedpush": {"gateway": "matrix"}}`. + static Future _gatewayFor(String endpoint) async { + final uri = Uri.parse(endpoint); + final candidate = uri.replace( + path: '/_matrix/push/v1/notify', + query: null, + fragment: null, + ); + try { + final response = await http + .get(candidate) + .timeout(const Duration(seconds: 10)); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json is Map && + json['unifiedpush'] is Map && + json['unifiedpush']['gateway'] == 'matrix') { + return candidate.toString(); + } + } + } catch (e) { + Logs().v('No Matrix gateway at ${candidate.origin}', e); + } + return fallbackGateway; + } + + static Future _onMessage(PushMessage message) async { + final client = _client; + Map notification; + try { + final json = jsonDecode(utf8.decode(message.content)); + notification = Map.from( + (json is Map && json['notification'] is Map) + ? json['notification'] as Map + : json as Map, + ); + } catch (e) { + Logs().w('Push that is not a Matrix notification', e); + return; + } + + final push = PushNotification.fromJson(notification); + final roomId = push.roomId; + // A push with no event is the homeserver saying the counts changed: + // everything was read on another device. + if (push.eventId == null || roomId == null) { + if ((push.counts?.unread ?? 0) == 0) await Notifications.clearAll(); + return; + } + if (client == null) return; + + try { + // Null when the event has already been seen (read on another device, + // or shown from the sync while the app was running). + final event = await client.getEventByPushNotification(push); + if (event != null) await Notifications.showEvent(event); + return; + } catch (e, s) { + Logs().w('Could not fetch the pushed event', e, s); + } + // Fetching failed (no network, or the session is gone): still say + // something arrived, without content. + final room = client.getRoomById(roomId); + await Notifications.show( + roomId: roomId, + title: + room?.getLocalizedDisplayname() ?? + push.roomName ?? + push.senderDisplayName ?? + 'Warp', + body: 'New message', + ); + } +} diff --git a/apps/mobile/lib/matrix/timeline.dart b/apps/mobile/lib/matrix/timeline.dart new file mode 100644 index 0000000..210018e --- /dev/null +++ b/apps/mobile/lib/matrix/timeline.dart @@ -0,0 +1,133 @@ +import 'package:matrix/matrix.dart'; + +/// How an event shows in the timeline. +enum EntryKind { + /// A bubble: a message, sticker, or an encrypted event. + message, + + /// A one-line notice in the middle: a join, a rename, encryption turned + /// on. + notice, + + /// Not shown: edits, reactions, redactions, call signalling, state + /// nobody needs to read. + hidden, +} + +/// The state events shown as notices. Everything else in a room's state +/// (power levels, join rules, ACLs, widgets) is hidden: it is what the room +/// info screen is for. +const noticeStateTypes = { + EventTypes.RoomMember, + EventTypes.RoomName, + EventTypes.RoomTopic, + EventTypes.RoomAvatar, + EventTypes.RoomCreate, + EventTypes.Encryption, + EventTypes.RoomTombstone, +}; + +EntryKind kindOf(Event event) { + if (event.relationshipType == RelationshipTypes.edit) return EntryKind.hidden; + switch (event.type) { + case EventTypes.Message: + case EventTypes.Sticker: + case EventTypes.Encrypted: + return EntryKind.message; + } + if (noticeStateTypes.contains(event.type)) { + if (event.type == EventTypes.RoomMember && _isProfileOnlyChange(event)) { + // A display name or avatar change of somebody already in the room is + // noise in a busy room; the bubble shows the new name anyway. + return EntryKind.hidden; + } + return EntryKind.notice; + } + return EntryKind.hidden; +} + +bool _isProfileOnlyChange(Event event) { + final prev = event.prevContent; + return prev != null && + prev['membership'] == 'join' && + event.content['membership'] == 'join'; +} + +/// One row of the timeline, as the screen draws it. +sealed class TimelineItem { + const TimelineItem(); +} + +class DayItem extends TimelineItem { + const DayItem(this.day); + final DateTime day; +} + +class EventItem extends TimelineItem { + const EventItem( + this.index, { + required this.kind, + required this.firstInGroup, + required this.lastInGroup, + }); + + /// The position of the event in the list given to [projectTimeline]. + final int index; + final EntryKind kind; + + /// Starts a run of messages from one sender: shows the name. + final bool firstInGroup; + + /// Ends a run: shows the avatar and the time. + final bool lastInGroup; +} + +/// What [projectTimeline] needs to know about an event. +typedef Entry = ({String sender, DateTime time, EntryKind kind}); + +/// Messages from one sender this close together are drawn as one group. +const groupGap = Duration(minutes: 5); + +/// Turns events, newest first as the SDK keeps them, into rows, newest first +/// for a reversed list: hidden events dropped, a [DayItem] above the first +/// event of each day, and runs of messages from one sender grouped. +List projectTimeline(List entries) { + final shown = [ + for (var i = 0; i < entries.length; i++) + if (entries[i].kind != EntryKind.hidden) i, + ]; + + bool sameGroup(int a, int b) { + final x = entries[a], y = entries[b]; + return x.kind == EntryKind.message && + y.kind == EntryKind.message && + x.sender == y.sender && + _sameDay(x.time, y.time) && + x.time.difference(y.time).abs() <= groupGap; + } + + final items = []; + for (var n = 0; n < shown.length; n++) { + final i = shown[n]; + final newer = n > 0 ? shown[n - 1] : null; + final older = n + 1 < shown.length ? shown[n + 1] : null; + items.add( + EventItem( + i, + kind: entries[i].kind, + firstInGroup: older == null || !sameGroup(i, older), + lastInGroup: newer == null || !sameGroup(i, newer), + ), + ); + final time = entries[i].time.toLocal(); + if (older == null || !_sameDay(entries[older].time, entries[i].time)) { + items.add(DayItem(DateTime(time.year, time.month, time.day))); + } + } + return items; +} + +bool _sameDay(DateTime a, DateTime b) { + final x = a.toLocal(), y = b.toLocal(); + return x.year == y.year && x.month == y.month && x.day == y.day; +} diff --git a/apps/mobile/lib/screens/encryption_screen.dart b/apps/mobile/lib/screens/encryption_screen.dart new file mode 100644 index 0000000..ef003d1 --- /dev/null +++ b/apps/mobile/lib/screens/encryption_screen.dart @@ -0,0 +1,374 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/encryption.dart'; + +import '../matrix/client.dart'; +import '../matrix/encryption.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../widgets/verification_sheet.dart'; + +/// Encryption for the account and this session: set it up, verify this +/// session (with the recovery key or another session), or reset it. +class EncryptionScreen extends ConsumerStatefulWidget { + const EncryptionScreen({super.key}); + + @override + ConsumerState createState() => _EncryptionScreenState(); +} + +class _EncryptionScreenState extends ConsumerState { + bool _busy = false; + String? _error; + + Future _run(Future Function() action) async { + setState(() { + _busy = true; + _error = null; + }); + try { + await action(); + } catch (e) { + if (mounted) setState(() => _error = describeError(e)); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + Future _setUp({required bool reset}) async { + final encryption = ref.read(clientProvider).encryption!; + if (reset) { + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Reset encryption?'), + content: const Text( + 'Only do this if you have lost your recovery key and have no ' + 'other verified session.\n\nYou get a new recovery key. Messages ' + 'only the old keys could read stay unreadable, and the people ' + "you've verified will need to verify you again.", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: context.warp.danger, + ), + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Reset'), + ), + ], + ), + ); + if (ok != true) return; + } + String? recoveryKey; + await _run(() async => recoveryKey = await setUpEncryption(encryption)); + final key = recoveryKey; + if (key != null && mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => _RecoveryKeyScreen(recoveryKey: key), + ), + ); + if (mounted) setState(() {}); + } + } + + Future _verifyWithKey() async { + final controller = TextEditingController(); + final input = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Enter your recovery key'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'The key you saved when you set up encryption. It looks ' + 'like EsTc 1234 โ€ฆ', + ), + const SizedBox(height: 16), + TextField( + controller: controller, + autofocus: true, + autocorrect: false, + enableSuggestions: false, + minLines: 2, + maxLines: 3, + style: const TextStyle(fontFamily: fontMono), + decoration: const InputDecoration(labelText: 'Recovery key'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(dialog, controller.text), + child: const Text('Verify'), + ), + ], + ), + ); + if (input == null || input.trim().isEmpty) return; + final encryption = ref.read(clientProvider).encryption!; + await _run(() async { + try { + await verifyWithRecoveryKey(encryption, input); + } on InvalidPassphraseException { + throw Exception("That recovery key isn't right."); + } + }); + if (mounted && _error == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'This session is verified. Older messages are being restored.', + ), + ), + ); + } + } + + Future _verifyWithSession() async { + final client = ref.read(clientProvider); + await _run(() async { + final keys = client.userDeviceKeys[client.userID!]; + if (keys == null) throw Exception('Your sessions are still loading.'); + final request = await keys.startVerification(); + if (mounted) await showVerificationSheet(context, request); + }); + } + + @override + Widget build(BuildContext context) { + ref.watch(syncProvider); + final client = ref.watch(clientProvider); + final encryption = client.encryption; + final c = context.warp; + final text = Theme.of(context).textTheme; + + final List body; + if (encryption == null) { + body = [ + const _Para('End-to-end encryption is not available in this build.'), + ]; + } else if (!encryption.crossSigning.enabled) { + body = [ + const _Para( + 'Encryption keeps your messages readable only by the people in ' + 'the conversation. Set it up once and you get a recovery key: with ' + 'it, any new session you sign in to can verify itself and read ' + 'your older messages.', + ), + const SizedBox(height: 8), + const _Para( + 'You will be asked for your password. Keep the recovery key ' + 'somewhere safe, like a password manager.', + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _busy ? null : () => _setUp(reset: false), + child: const Text('Set up encryption'), + ), + ]; + } else if (client.isUnknownSession) { + body = [ + const _Para( + 'Prove this session is you, so it can read your encrypted messages ' + 'and the people you talk to trust it.', + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _busy ? null : _verifyWithSession, + icon: const Icon(Icons.devices_outlined), + label: const Text('Use another session'), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: _busy ? null : _verifyWithKey, + icon: const Icon(Icons.key_outlined), + label: const Text('Use recovery key'), + ), + const SizedBox(height: 32), + TextButton( + onPressed: _busy ? null : () => _setUp(reset: true), + style: TextButton.styleFrom(foregroundColor: c.danger), + child: const Text('Lost your recovery key? Reset encryption'), + ), + ]; + } else { + body = [ + _Status( + ok: true, + title: 'This session is verified', + detail: 'It can read your encrypted messages.', + ), + _Status( + ok: encryption.keyManager.enabled, + title: encryption.keyManager.enabled + ? 'Key backup is on' + : 'Key backup is off', + detail: encryption.keyManager.enabled + ? 'Your message keys are backed up, encrypted with your ' + 'recovery key.' + : 'New sessions may not be able to read older messages.', + ), + const SizedBox(height: 32), + OutlinedButton( + onPressed: _busy ? null : () => _setUp(reset: true), + style: OutlinedButton.styleFrom(foregroundColor: c.danger), + child: const Text('Reset encryption'), + ), + ]; + } + + return Scaffold( + appBar: AppBar(title: const Text('Encryption')), + body: ListView( + padding: const EdgeInsets.all(24), + children: [ + if (_busy) ...[ + const LinearProgressIndicator(minHeight: 2), + const SizedBox(height: 16), + ], + if (_error != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: c.dangerSoft, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _error!, + style: text.bodyMedium?.copyWith(color: c.danger), + ), + ), + const SizedBox(height: 16), + ], + ...body, + ], + ), + ); + } +} + +class _Para extends StatelessWidget { + const _Para(this.text); + final String text; + + @override + Widget build(BuildContext context) => + Text(text, style: Theme.of(context).textTheme.bodyLarge); +} + +class _Status extends StatelessWidget { + const _Status({required this.ok, required this.title, required this.detail}); + final bool ok; + final String title; + final String detail; + + @override + Widget build(BuildContext context) { + final c = context.warp; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon( + ok ? Icons.check_circle_outline : Icons.error_outline, + color: ok ? c.tealText : c.warning, + ), + title: Text(title), + subtitle: Text(detail), + ); + } +} + +/// The new recovery key, shown once. The user has to say they saved it. +class _RecoveryKeyScreen extends StatefulWidget { + const _RecoveryKeyScreen({required this.recoveryKey}); + final String recoveryKey; + + @override + State<_RecoveryKeyScreen> createState() => _RecoveryKeyScreenState(); +} + +class _RecoveryKeyScreenState extends State<_RecoveryKeyScreen> { + bool _saved = false; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + return PopScope( + canPop: _saved, + child: Scaffold( + appBar: AppBar( + title: const Text('Your recovery key'), + automaticallyImplyLeading: false, + ), + body: ListView( + padding: const EdgeInsets.all(24), + children: [ + Text( + 'Save this key somewhere safe, like a password manager. It is ' + 'the only way to read your encrypted messages on a new session ' + "if you aren't signed in anywhere else. It won't be shown again.", + style: text.bodyLarge, + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: c.surfaceSunken, + border: Border.all(color: c.line), + borderRadius: BorderRadius.circular(12), + ), + child: SelectableText( + widget.recoveryKey, + textAlign: TextAlign.center, + style: text.titleMedium?.copyWith( + fontFamily: fontMono, + letterSpacing: 1, + height: 1.6, + ), + ), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: widget.recoveryKey)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Recovery key copied')), + ); + }, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + const SizedBox(height: 24), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: _saved, + onChanged: (v) => setState(() => _saved = v ?? false), + title: const Text("I've saved my recovery key"), + controlAffinity: ListTileControlAffinity.leading, + ), + const SizedBox(height: 12), + FilledButton( + onPressed: _saved ? () => Navigator.pop(context) : null, + child: const Text('Done'), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/lib/screens/home_screen.dart b/apps/mobile/lib/screens/home_screen.dart new file mode 100644 index 0000000..54a23f3 --- /dev/null +++ b/apps/mobile/lib/screens/home_screen.dart @@ -0,0 +1,553 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../util/format.dart'; +import '../matrix/client.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../widgets/avatar.dart'; +import '../widgets/new_chat_sheet.dart'; +import 'encryption_screen.dart'; +import 'room_screen.dart'; +import 'settings_screen.dart'; + +enum _Filter { all, unread, people, rooms } + +class HomeScreen extends ConsumerStatefulWidget { + const HomeScreen({super.key}); + + @override + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState { + _Filter _filter = _Filter.all; + final _search = TextEditingController(); + bool _searching = false; + + @override + void dispose() { + _search.dispose(); + super.dispose(); + } + + bool _matches(Room room) { + if (room.isSpace) return false; + final query = _search.text.trim().toLowerCase(); + if (query.isNotEmpty && + !room.getLocalizedDisplayname().toLowerCase().contains(query) && + !room.canonicalAlias.toLowerCase().contains(query)) { + return false; + } + return switch (_filter) { + _Filter.all => true, + _Filter.unread => room.isUnreadOrInvited, + _Filter.people => room.isDirectChat, + _Filter.rooms => !room.isDirectChat, + }; + } + + @override + Widget build(BuildContext context) { + ref.watch(syncProvider); + final client = ref.watch(clientProvider); + final offline = ref.watch(syncErrorProvider).value == true; + final rooms = client.rooms.where(_matches).toList(); + final invites = rooms + .where((r) => r.membership == Membership.invite) + .toList(); + final joined = rooms.where((r) => r.membership == Membership.join).toList(); + + return Scaffold( + appBar: AppBar( + titleSpacing: 16, + title: _searching + ? TextField( + controller: _search, + autofocus: true, + onChanged: (_) => setState(() {}), + decoration: const InputDecoration( + hintText: 'Search rooms', + isDense: true, + ), + ) + : Row( + children: [ + Image.asset( + 'assets/logo/warp-symbol.png', + width: 28, + height: 28, + ), + const SizedBox(width: 10), + const Text('Chats'), + ], + ), + actions: [ + IconButton( + tooltip: _searching ? 'Close search' : 'Search', + icon: Icon(_searching ? Icons.close : Icons.search), + onPressed: () => setState(() { + _searching = !_searching; + if (!_searching) _search.clear(); + }), + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + tooltip: 'Settings', + icon: _OwnAvatar(client: client), + onPressed: () => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const SettingsScreen())), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + tooltip: 'New chat', + onPressed: () => showNewChatSheet(context), + child: const Icon(Icons.edit_outlined), + ), + body: Column( + children: [ + if (offline) + const _Banner( + icon: Icons.cloud_off_outlined, + text: "Can't reach the server. Retryingโ€ฆ", + ), + _SecurityBanner(client: client), + SizedBox( + height: 52, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + children: [ + for (final f in _Filter.values) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ChoiceChip( + label: Text(switch (f) { + _Filter.all => 'All', + _Filter.unread => 'Unread', + _Filter.people => 'People', + _Filter.rooms => 'Rooms', + }), + selected: _filter == f, + showCheckmark: false, + onSelected: (_) => setState(() => _filter = f), + ), + ), + ], + ), + ), + Expanded( + child: rooms.isEmpty + ? _Empty( + loading: client.prevBatch == null, + filtered: _filter != _Filter.all || _search.text.isNotEmpty, + ) + : ListView( + padding: const EdgeInsets.only(bottom: 88), + children: [ + if (invites.isNotEmpty) ...[ + const _SectionHeader('Invites'), + for (final room in invites) _InviteTile(room: room), + if (joined.isNotEmpty) const _SectionHeader('Chats'), + ], + for (final room in joined) RoomTile(room: room), + ], + ), + ), + ], + ), + ); + } +} + +class _OwnAvatar extends StatelessWidget { + const _OwnAvatar({required this.client}); + final Client client; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: client.fetchOwnProfile(), + builder: (context, snapshot) { + final profile = snapshot.data; + return Avatar( + name: profile?.displayName ?? client.userID ?? '?', + seed: client.userID ?? '', + mxc: profile?.avatarUrl, + size: 32, + ); + }, + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader(this.text); + final String text; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + text.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + letterSpacing: 0.6, + fontWeight: FontWeight.w600, + ), + ), + ); +} + +class _Banner extends StatelessWidget { + const _Banner({ + required this.icon, + required this.text, + this.action, + this.onTap, + this.tone = _Tone.warning, + }); + final IconData icon; + final String text; + final String? action; + final VoidCallback? onTap; + final _Tone tone; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final (bg, fg) = switch (tone) { + _Tone.warning => (c.warningSoft, c.warning), + _Tone.info => (c.tealSoft, c.tealText), + }; + return Material( + color: bg, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Icon(icon, size: 20, color: fg), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: fg), + ), + ), + if (action != null) + Text( + action!, + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(color: fg), + ), + ], + ), + ), + ), + ); + } +} + +enum _Tone { warning, info } + +/// Asks for encryption to be set up on an account that has none, or for +/// this session to be verified when the account has it and this session +/// can't yet read the older messages. +class _SecurityBanner extends StatelessWidget { + const _SecurityBanner({required this.client}); + final Client client; + + @override + Widget build(BuildContext context) { + final encryption = client.encryption; + if (encryption == null || client.prevBatch == null) { + return const SizedBox.shrink(); + } + void open() => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const EncryptionScreen())); + if (!encryption.crossSigning.enabled) { + return _Banner( + icon: Icons.key_outlined, + text: + 'Set up encryption to keep your messages readable on new ' + 'sessions.', + action: 'Set up', + tone: _Tone.info, + onTap: open, + ); + } + if (client.isUnknownSession) { + return _Banner( + icon: Icons.verified_user_outlined, + text: 'Verify this session to read your encrypted messages.', + action: 'Verify', + onTap: open, + ); + } + return const SizedBox.shrink(); + } +} + +class _Empty extends StatelessWidget { + const _Empty({required this.loading, required this.filtered}); + final bool loading; + final bool filtered; + + @override + Widget build(BuildContext context) { + if (loading) return const Center(child: CircularProgressIndicator()); + final text = Theme.of(context).textTheme; + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.forum_outlined, + size: 48, + color: context.warp.lineStrong, + ), + const SizedBox(height: 16), + Text( + filtered ? 'Nothing here' : 'No chats yet', + style: text.titleMedium, + ), + const SizedBox(height: 4), + Text( + filtered + ? 'No rooms match.' + : 'Start a chat with someone, or create or join a room.', + textAlign: TextAlign.center, + style: text.bodySmall, + ), + ], + ), + ), + ); + } +} + +class RoomTile extends StatelessWidget { + const RoomTile({super.key, required this.room}); + final Room room; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + final last = room.lastEvent; + final unread = room.notificationCount; + final highlight = room.highlightCount > 0; + final bold = room.isUnread; + final preview = last == null + ? '' + : last.calcLocalizedBodyFallback( + const MatrixDefaultLocalizations(), + withSenderNamePrefix: !room.isDirectChat, + hideReply: true, + hideEdit: true, + plaintextBody: false, + removeMarkdown: true, + ); + + return InkWell( + onTap: () => Navigator.of(context).push(RoomScreen.route(room.id)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Avatar( + name: room.getLocalizedDisplayname(), + seed: room.id, + mxc: room.avatar, + size: 48, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + room.getLocalizedDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.titleSmall?.copyWith( + fontSize: 15, + fontWeight: bold + ? FontWeight.w700 + : FontWeight.w600, + ), + ), + ), + if (last != null) + Text( + formatListTime(last.originServerTs), + style: text.labelSmall?.copyWith( + color: bold ? c.tealText : c.inkMuted, + ), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + if (room.encrypted) ...[ + Icon(Icons.lock_outline, size: 13, color: c.inkMuted), + const SizedBox(width: 4), + ], + Expanded( + child: Text( + preview, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.bodySmall?.copyWith( + color: bold ? c.ink : c.inkMuted, + ), + ), + ), + if (unread > 0 || room.markedUnread) + Container( + margin: const EdgeInsets.only(left: 8), + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 2, + ), + constraints: const BoxConstraints(minWidth: 20), + decoration: BoxDecoration( + color: highlight ? c.danger : c.tealStrong, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + unread > 99 + ? '99+' + : unread > 0 + ? '$unread' + : '', + textAlign: TextAlign.center, + style: text.labelMedium?.copyWith( + color: highlight ? Colors.white : c.onTeal, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _InviteTile extends ConsumerStatefulWidget { + const _InviteTile({required this.room}); + final Room room; + + @override + ConsumerState<_InviteTile> createState() => _InviteTileState(); +} + +class _InviteTileState extends ConsumerState<_InviteTile> { + bool _busy = false; + + Future _act(Future Function() action) async { + setState(() => _busy = true); + try { + await action(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(e)))); + } + } finally { + if (mounted) setState(() => _busy = false); + } + } + + @override + Widget build(BuildContext context) { + final room = widget.room; + final text = Theme.of(context).textTheme; + final inviter = room + .getState(EventTypes.RoomMember, room.client.userID!) + ?.senderId; + final inviterName = inviter == null + ? null + : room.unsafeGetUserFromMemoryOrFallback(inviter).calcDisplayname(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Avatar( + name: room.getLocalizedDisplayname(), + seed: room.id, + mxc: room.avatar, + size: 48, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + room.getLocalizedDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.titleSmall?.copyWith(fontSize: 15), + ), + Text( + inviterName == null + ? 'Invited you' + : '$inviterName invited you', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.bodySmall, + ), + ], + ), + ), + if (_busy) + const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else ...[ + IconButton( + tooltip: 'Decline', + icon: const Icon(Icons.close), + onPressed: () => _act(room.leave), + ), + IconButton.filled( + tooltip: 'Join', + icon: const Icon(Icons.check), + onPressed: () => _act(() async { + await room.join(); + if (context.mounted) { + Navigator.of(context).push(RoomScreen.route(room.id)); + } + }), + ), + ], + ], + ), + ); + } +} diff --git a/apps/mobile/lib/screens/login_screen.dart b/apps/mobile/lib/screens/login_screen.dart new file mode 100644 index 0000000..65b13ee --- /dev/null +++ b/apps/mobile/lib/screens/login_screen.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../state.dart'; +import '../theme.dart'; +import 'register_screen.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final _server = TextEditingController(); + final _username = TextEditingController(); + final _password = TextEditingController(); + bool _busy = false; + String? _error; + + @override + void dispose() { + _server.dispose(); + _username.dispose(); + _password.dispose(); + super.dispose(); + } + + Future _signIn() async { + final client = ref.read(clientProvider); + final username = _username.text.trim(); + final server = _server.text.trim().isNotEmpty + ? _server.text.trim() + : serverOfUserId(username); + if (server == null) { + setState(() => _error = 'Enter your server, or your full user ID.'); + return; + } + if (username.isEmpty || _password.text.isEmpty) { + setState(() => _error = 'Enter your username and password.'); + return; + } + setState(() { + _busy = true; + _error = null; + }); + try { + await resolveHomeserver(client, server); + await client.login( + LoginType.mLoginPassword, + identifier: AuthenticationUserIdentifier(user: username), + password: _password.text, + initialDeviceDisplayName: deviceDisplayName, + ); + // The app switches to the room list when the login state changes. + } catch (e) { + if (!mounted) return; + setState(() { + _busy = false; + _error = e is MatrixException && e.error == MatrixError.M_FORBIDDEN + ? 'Wrong username or password.' + : describeError(e); + }); + } + } + + @override + Widget build(BuildContext context) { + return AuthLayout( + title: 'Sign in', + subtitle: 'Use your Matrix account on any server.', + error: _error, + children: [ + TextField( + controller: _server, + enabled: !_busy, + keyboardType: TextInputType.url, + autocorrect: false, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Server', + hintText: 'example.org', + helperText: 'Leave empty if you type your full user ID below.', + ), + ), + const SizedBox(height: 16), + AutofillGroup( + child: Column( + children: [ + TextField( + controller: _username, + enabled: !_busy, + autocorrect: false, + autofillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Username', + hintText: 'alice or @alice:example.org', + ), + ), + const SizedBox(height: 16), + TextField( + controller: _password, + enabled: !_busy, + obscureText: true, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + onSubmitted: (_) => _signIn(), + decoration: const InputDecoration(labelText: 'Password'), + ), + ], + ), + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _busy ? null : _signIn, + child: _busy + ? const SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Sign in'), + ), + const SizedBox(height: 12), + TextButton( + onPressed: _busy + ? null + : () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => RegisterScreen(server: _server.text), + ), + ), + child: const Text('Create an account'), + ), + ], + ); + } +} + +/// The frame of the sign-in and sign-up screens: logo, title, form. +class AuthLayout extends StatelessWidget { + const AuthLayout({ + super.key, + required this.title, + required this.subtitle, + required this.children, + this.error, + }); + + final String title; + final String subtitle; + final String? error; + final List children; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + return Scaffold( + appBar: Navigator.of(context).canPop() + ? AppBar(shape: const Border()) + : null, + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Image.asset( + 'assets/logo/warp-symbol.png', + width: 40, + height: 40, + ), + const SizedBox(width: 12), + Text( + 'Warp', + style: text.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 32), + Text(title, style: text.titleLarge), + const SizedBox(height: 4), + Text(subtitle, style: text.bodySmall), + const SizedBox(height: 24), + if (error != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: c.dangerSoft, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + error!, + style: text.bodyMedium?.copyWith(color: c.danger), + ), + ), + const SizedBox(height: 16), + ], + ...children, + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/lib/screens/register_screen.dart b/apps/mobile/lib/screens/register_screen.dart new file mode 100644 index 0000000..b30b003 --- /dev/null +++ b/apps/mobile/lib/screens/register_screen.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../state.dart'; +import 'login_screen.dart'; + +/// The registration token stage (Matrix 1.2). +const _tokenStage = 'm.login.registration_token'; + +/// Stages the app can complete. Anything else (a captcha, an email) needs a +/// browser; such a server's sign-up is done elsewhere. +const _supportedStages = {AuthenticationTypes.dummy, _tokenStage}; + +class RegisterScreen extends ConsumerStatefulWidget { + const RegisterScreen({super.key, this.server = ''}); + final String server; + + @override + ConsumerState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends ConsumerState { + late final _server = TextEditingController(text: widget.server); + final _username = TextEditingController(); + final _password = TextEditingController(); + final _confirm = TextEditingController(); + final _token = TextEditingController(); + bool _busy = false; + bool _needsToken = false; + String? _error; + + @override + void dispose() { + for (final c in [_server, _username, _password, _confirm, _token]) { + c.dispose(); + } + super.dispose(); + } + + Future _signUp() async { + final client = ref.read(clientProvider); + final username = _username.text.trim().replaceFirst(RegExp(r'^@'), ''); + String? problem; + if (_server.text.trim().isEmpty) { + problem = 'Enter the server to sign up on.'; + } else if (username.isEmpty) { + problem = 'Choose a username.'; + } else if (_password.text.length < 8) { + problem = 'Use a password of at least 8 characters.'; + } else if (_password.text != _confirm.text) { + problem = "The passwords don't match."; + } else if (_needsToken && _token.text.trim().isEmpty) { + problem = 'Enter your registration token.'; + } + if (problem != null) { + setState(() => _error = problem); + return; + } + + setState(() { + _busy = true; + _error = null; + }); + try { + await resolveHomeserver(client, _server.text); + await _register(client, username); + } catch (e) { + if (!mounted) return; + setState(() { + _busy = false; + _error = describeError(e); + }); + } + } + + /// Registers, walking the server's user-interactive authentication: the + /// first attempt returns the flows, then each stage of a flow the app can + /// do is completed in turn. + Future _register(Client client, String username) async { + AuthenticationData? auth; + for (var attempt = 0; attempt < 5; attempt++) { + try { + await client.register( + username: username, + password: _password.text, + initialDeviceDisplayName: deviceDisplayName, + auth: auth, + ); + return; + } on MatrixException catch (e) { + if (!e.requireAdditionalAuthentication) rethrow; + final flows = e.authenticationFlows ?? []; + final done = e.completedAuthenticationFlows; + final flow = flows + .where((f) => f.stages.every(_supportedStages.contains)) + .firstOrNull; + if (flow == null) { + if (flows.isEmpty) rethrow; + throw Exception( + 'This server asks for a sign-up step Warp can\'t do yet. ' + 'Sign up on the server\'s website, then sign in here.', + ); + } + final next = flow.stages.where((s) => !done.contains(s)).firstOrNull; + if (next == null) rethrow; + if (next == _tokenStage) { + final token = _token.text.trim(); + if (token.isEmpty) { + setState(() => _needsToken = true); + throw Exception('This server needs a registration token.'); + } + auth = AuthenticationData( + type: _tokenStage, + session: e.session, + additionalFields: {'token': token}, + ); + } else { + auth = AuthenticationData(type: next, session: e.session); + } + } + } + throw Exception('Sign-up did not finish. Try again.'); + } + + @override + Widget build(BuildContext context) { + return AuthLayout( + title: 'Create an account', + subtitle: 'Your user ID will be @username:server.', + error: _error, + children: [ + TextField( + controller: _server, + enabled: !_busy, + keyboardType: TextInputType.url, + autocorrect: false, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Server', + hintText: 'example.org', + ), + ), + const SizedBox(height: 16), + TextField( + controller: _username, + enabled: !_busy, + autocorrect: false, + autofillHints: const [AutofillHints.newUsername], + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Username'), + ), + const SizedBox(height: 16), + TextField( + controller: _password, + enabled: !_busy, + obscureText: true, + autofillHints: const [AutofillHints.newPassword], + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Password'), + ), + const SizedBox(height: 16), + TextField( + controller: _confirm, + enabled: !_busy, + obscureText: true, + textInputAction: _needsToken + ? TextInputAction.next + : TextInputAction.done, + onSubmitted: _needsToken ? null : (_) => _signUp(), + decoration: const InputDecoration(labelText: 'Confirm password'), + ), + if (_needsToken) ...[ + const SizedBox(height: 16), + TextField( + controller: _token, + enabled: !_busy, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.done, + onSubmitted: (_) => _signUp(), + decoration: const InputDecoration( + labelText: 'Registration token', + helperText: "The server's admin gives these out.", + ), + ), + ], + const SizedBox(height: 24), + FilledButton( + onPressed: _busy ? null : _signUp, + child: _busy + ? const SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Create account'), + ), + ], + ); + } +} diff --git a/apps/mobile/lib/screens/room_info_screen.dart b/apps/mobile/lib/screens/room_info_screen.dart new file mode 100644 index 0000000..add85a2 --- /dev/null +++ b/apps/mobile/lib/screens/room_info_screen.dart @@ -0,0 +1,425 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../widgets/avatar.dart'; +import '../widgets/verification_sheet.dart'; + +class RoomInfoScreen extends ConsumerStatefulWidget { + const RoomInfoScreen({super.key, required this.roomId}); + final String roomId; + + @override + ConsumerState createState() => _RoomInfoScreenState(); +} + +class _RoomInfoScreenState extends ConsumerState { + late Future> _members; + + Room get _room => ref.read(clientProvider).getRoomById(widget.roomId)!; + + @override + void initState() { + super.initState(); + _members = _room.requestParticipants(); + } + + void _snack(Object error) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(error)))); + } + + Future _ask(String title, String label, {String initial = ''}) { + final controller = TextEditingController(text: initial); + return showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration(labelText: label), + onSubmitted: (v) => Navigator.pop(dialog, v), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(dialog, controller.text), + child: const Text('Save'), + ), + ], + ), + ); + } + + Future _invite() async { + final room = _room; + var id = (await _ask('Invite someone', 'User ID'))?.trim(); + if (id == null || id.isEmpty) return; + if (!id.startsWith('@')) id = '@$id'; + if (!id.contains(':')) id = '$id:${room.client.userID!.domain}'; + try { + await room.invite(id); + setState(() => _members = room.requestParticipants()); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Invited $id'))); + } + } catch (e) { + _snack(e); + } + } + + Future _leave() async { + final room = _room; + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Leave room?'), + content: Text( + room.joinRules == JoinRules.public + ? 'You can join again later.' + : "It's private: you'll need a new invite to come back.", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: context.warp.danger), + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Leave'), + ), + ], + ), + ); + if (ok != true) return; + try { + await room.leave(); + if (mounted) Navigator.of(context).popUntil((r) => r.isFirst); + } catch (e) { + _snack(e); + } + } + + Future _enableEncryption() async { + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Turn on encryption?'), + content: const Text( + "Once it's on, it can't be turned off. Messages sent before stay " + 'unencrypted.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Turn on'), + ), + ], + ), + ); + if (ok != true) return; + try { + await _room.enableEncryption(); + } catch (e) { + _snack(e); + } + } + + @override + Widget build(BuildContext context) { + ref.watch(syncProvider); + final client = ref.watch(clientProvider); + final room = client.getRoomById(widget.roomId); + if (room == null) return Scaffold(appBar: AppBar()); + final c = context.warp; + final text = Theme.of(context).textTheme; + final name = room.getLocalizedDisplayname(); + + return Scaffold( + appBar: AppBar(title: const Text('Room info')), + body: ListView( + children: [ + const SizedBox(height: 24), + Center( + child: Avatar( + name: name, + seed: room.id, + mxc: room.avatar, + size: 88, + ), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + name, + textAlign: TextAlign.center, + style: text.headlineSmall, + ), + ), + if (room.canonicalAlias.isNotEmpty) + Text( + room.canonicalAlias, + textAlign: TextAlign.center, + style: text.bodySmall?.copyWith(fontFamily: fontMono), + ), + if (room.topic.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 0), + child: Text( + room.topic, + textAlign: TextAlign.center, + style: text.bodyMedium, + ), + ), + const SizedBox(height: 16), + const Divider(), + ListTile( + leading: Icon( + room.encrypted ? Icons.lock_outline : Icons.lock_open_outlined, + color: room.encrypted ? c.tealText : c.warning, + ), + title: Text( + room.encrypted ? 'End-to-end encrypted' : 'Not encrypted', + ), + subtitle: Text( + room.encrypted + ? 'Only the people in the room can read its messages.' + : 'The server can read messages in this room.', + ), + trailing: + !room.encrypted && + client.encryptionEnabled && + room.canChangeStateEvent(EventTypes.Encryption) + ? TextButton( + onPressed: _enableEncryption, + child: const Text('Turn on'), + ) + : null, + ), + ListTile( + leading: const Icon(Icons.notifications_outlined), + title: const Text('Notifications'), + trailing: DropdownButton( + value: room.pushRuleState, + underline: const SizedBox.shrink(), + items: const [ + DropdownMenuItem( + value: PushRuleState.notify, + child: Text('All messages'), + ), + DropdownMenuItem( + value: PushRuleState.mentionsOnly, + child: Text('Mentions only'), + ), + DropdownMenuItem( + value: PushRuleState.dontNotify, + child: Text('Off'), + ), + ], + onChanged: (v) { + if (v != null) room.setPushRuleState(v).catchError(_snack); + }, + ), + ), + if (room.canChangeStateEvent(EventTypes.RoomName)) + ListTile( + leading: const Icon(Icons.edit_outlined), + title: const Text('Rename'), + onTap: () async { + final value = await _ask('Rename room', 'Name', initial: name); + if (value != null && value.trim().isNotEmpty) { + room.setName(value.trim()).then((_) {}, onError: _snack); + } + }, + ), + if (room.canChangeStateEvent(EventTypes.RoomTopic)) + ListTile( + leading: const Icon(Icons.subject), + title: const Text('Change topic'), + onTap: () async { + final value = await _ask('Topic', 'Topic', initial: room.topic); + if (value != null) { + room + .setDescription(value.trim()) + .then((_) {}, onError: _snack); + } + }, + ), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 8, 4), + child: Row( + children: [ + Expanded(child: Text('Members', style: text.titleMedium)), + if (room.canInvite) + TextButton.icon( + onPressed: _invite, + icon: const Icon(Icons.person_add_alt_1_outlined), + label: const Text('Invite'), + ), + ], + ), + ), + FutureBuilder>( + future: _members, + builder: (context, snapshot) { + final members = snapshot.data; + if (members == null) { + return const Padding( + padding: EdgeInsets.all(24), + child: Center(child: CircularProgressIndicator()), + ); + } + members.sort((a, b) { + final p = b.powerLevel.level.compareTo(a.powerLevel.level); + return p != 0 + ? p + : a.calcDisplayname().toLowerCase().compareTo( + b.calcDisplayname().toLowerCase(), + ); + }); + return Column( + children: [ + for (final m in members) _MemberTile(user: m, room: room), + ], + ); + }, + ), + const Divider(), + ListTile( + leading: Icon(Icons.logout, color: c.danger), + title: Text('Leave room', style: TextStyle(color: c.danger)), + onTap: _leave, + ), + const SizedBox(height: 24), + ], + ), + ); + } +} + +class _MemberTile extends StatelessWidget { + const _MemberTile({required this.user, required this.room}); + final User user; + final Room room; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final client = room.client; + final keys = client.userDeviceKeys[user.id]; + final me = user.id == client.userID; + final verified = keys?.verified == UserVerifiedStatus.verified; + final role = switch (user.powerLevel.role) { + PowerLevelRole.owner || PowerLevelRole.admin => 'Admin', + PowerLevelRole.moderator => 'Moderator', + PowerLevelRole.user => null, + }; + return ListTile( + leading: Avatar( + name: user.calcDisplayname(), + seed: user.id, + mxc: user.avatarUrl, + ), + title: Row( + children: [ + Flexible( + child: Text( + user.calcDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (room.encrypted && keys != null && !me) ...[ + const SizedBox(width: 6), + Icon( + verified ? Icons.verified_user : Icons.shield_outlined, + size: 16, + color: verified ? c.tealText : c.inkMuted, + ), + ], + ], + ), + subtitle: Text( + [ + user.id, + if (user.membership == Membership.invite) 'invited', + ?role, + ].join(' ยท '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: me || keys == null || !client.encryptionEnabled + ? null + : () => _showMember(context, keys, verified), + ); + } + + Future _showMember( + BuildContext context, + DeviceKeysList keys, + bool verified, + ) async { + final verify = await showModalBottomSheet( + context: context, + builder: (sheet) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + user.calcDisplayname(), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + verified + ? "You've verified ${user.calcDisplayname()}. Messages " + 'from their verified sessions are trusted.' + : 'Verify ${user.calcDisplayname()} to be sure their ' + 'messages come from them. You compare emoji on ' + 'both your screens, ideally in person or on a call.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + FilledButton( + onPressed: () => Navigator.pop(sheet, true), + child: Text(verified ? 'Verify again' : 'Verify'), + ), + ], + ), + ), + ), + ); + if (verify != true || !context.mounted) return; + try { + final request = await keys.startVerification(); + if (context.mounted) await showVerificationSheet(context, request); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(e)))); + } + } + } +} diff --git a/apps/mobile/lib/screens/room_screen.dart b/apps/mobile/lib/screens/room_screen.dart new file mode 100644 index 0000000..5b2ef7f --- /dev/null +++ b/apps/mobile/lib/screens/room_screen.dart @@ -0,0 +1,708 @@ +import 'dart:async'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../matrix/notifications.dart'; +import '../matrix/timeline.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../util/format.dart'; +import '../widgets/avatar.dart'; +import '../widgets/message.dart'; +import 'room_info_screen.dart'; + +class RoomScreen extends ConsumerStatefulWidget { + const RoomScreen({super.key, required this.roomId}); + final String roomId; + + static Route route(String roomId) => MaterialPageRoute( + settings: RouteSettings(name: 'room/$roomId'), + builder: (_) => RoomScreen(roomId: roomId), + ); + + @override + ConsumerState createState() => _RoomScreenState(); +} + +class _RoomScreenState extends ConsumerState + with WidgetsBindingObserver { + Timeline? _timeline; + Object? _loadError; + final _scroll = ScrollController(); + final _composer = TextEditingController(); + final _focus = FocusNode(); + bool _loadingHistory = false; + Timer? _typingTimer; + bool _typing = false; + StreamSubscription? _syncSub; + + Room? get _room => ref.read(clientProvider).getRoomById(widget.roomId); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + Notifications.activeRoomId = widget.roomId; + Notifications.clearRoom(widget.roomId); + _scroll.addListener(_onScroll); + _composer.addListener(_onComposerChanged); + _syncSub = ref.read(clientProvider).onSync.stream.listen((_) { + if (mounted) setState(() {}); + }); + _load(); + } + + Future _load() async { + final room = _room; + if (room == null) return; + try { + final timeline = await room.getTimeline( + onUpdate: _onTimelineUpdate, + onNewEvent: _markRead, + ); + if (!mounted) { + timeline.cancelSubscriptions(); + return; + } + setState(() => _timeline = timeline); + _markRead(); + // A short room may not fill the screen: fetch more until it does. + WidgetsBinding.instance.addPostFrameCallback((_) => _onScroll()); + } catch (e) { + if (mounted) setState(() => _loadError = e); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + if (Notifications.activeRoomId == widget.roomId) { + Notifications.activeRoomId = null; + } + _timeline?.cancelSubscriptions(); + _syncSub?.cancel(); + _typingTimer?.cancel(); + if (_typing) _room?.setTyping(false); + _scroll.dispose(); + _composer.dispose(); + _focus.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) _markRead(); + } + + void _onTimelineUpdate() { + if (mounted) setState(() {}); + } + + /// Sends a read receipt for the newest event, while the room is on screen + /// and the app is in front. + void _markRead() { + final timeline = _timeline; + final room = _room; + if (timeline == null || room == null) return; + if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { + return; + } + if (room.membership != Membership.join) return; + if (room.notificationCount == 0 && + !room.markedUnread && + room.receiptState.global.latestOwnReceipt?.eventId == + timeline.events.firstOrNull?.eventId) { + return; + } + if (room.markedUnread) room.markUnread(false); + timeline.setReadMarker().catchError((Object e) { + Logs().w('Could not send a read receipt', e); + }); + Notifications.clearRoom(room.id); + } + + void _onScroll() { + final timeline = _timeline; + if (timeline == null || _loadingHistory || !timeline.canRequestHistory) { + return; + } + if (!_scroll.hasClients) return; + final position = _scroll.position; + if (position.pixels < position.maxScrollExtent - 600) return; + setState(() => _loadingHistory = true); + timeline + .requestHistory() + .catchError((Object e) => Logs().w('Could not load history', e)) + .whenComplete(() { + if (mounted) setState(() => _loadingHistory = false); + }); + } + + void _onComposerChanged() { + final room = _room; + if (room == null) return; + final hasText = _composer.text.trim().isNotEmpty; + if (hasText && !_typing) { + _typing = true; + room.setTyping(true, timeout: 30000).catchError((_) {}); + } + _typingTimer?.cancel(); + if (hasText) { + _typingTimer = Timer(const Duration(seconds: 5), _stopTyping); + } else { + _stopTyping(); + } + setState(() {}); + } + + void _stopTyping() { + if (!_typing) return; + _typing = false; + _room?.setTyping(false).catchError((_) {}); + } + + Future _send() async { + final room = _room; + final text = _composer.text.trim(); + if (room == null || text.isEmpty) return; + _composer.clear(); + _stopTyping(); + try { + // The pending message shows at once as "Sending"; a failure leaves it + // in the timeline with Retry and Delete. + await room.sendTextEvent(text); + } catch (e) { + _snack(describeError(e)); + } + } + + Future _sendFile(MatrixFile file) async { + final room = _room; + if (room == null) return; + try { + await room.sendFileEvent( + file, + shrinkImageMaxDimension: 1600, + extraContent: {'body': file.name}, + ); + } catch (e) { + _snack(describeError(e)); + } + } + + Future _attach() async { + final choice = await showModalBottomSheet( + context: context, + builder: (sheet) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.photo_library_outlined), + title: const Text('Photo or video'), + onTap: () => Navigator.pop(sheet, 'gallery'), + ), + ListTile( + leading: const Icon(Icons.photo_camera_outlined), + title: const Text('Take a photo'), + onTap: () => Navigator.pop(sheet, 'camera'), + ), + ListTile( + leading: const Icon(Icons.insert_drive_file_outlined), + title: const Text('File'), + onTap: () => Navigator.pop(sheet, 'file'), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + if (choice == null) return; + try { + if (choice == 'file') { + for (final f in await FilePicker.pickFiles()) { + final bytes = await f.xFile.readAsBytes(); + await _sendFile(MatrixFile.fromMimeType(bytes: bytes, name: f.name)); + } + return; + } + final picker = ImagePicker(); + final picked = choice == 'camera' + ? [ + ?await picker.pickImage( + source: ImageSource.camera, + imageQuality: 90, + ), + ] + : await picker.pickMultipleMedia(imageQuality: 90); + for (final x in picked) { + final bytes = await x.readAsBytes(); + await _sendFile( + MatrixFile.fromMimeType( + bytes: bytes, + name: x.name, + mimeType: x.mimeType, + ), + ); + } + } on PlatformException catch (e) { + _snack(e.message ?? "Couldn't open that."); + } + } + + void _snack(String text) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); + } + + @override + Widget build(BuildContext context) { + final room = _room; + if (room == null) { + return Scaffold( + appBar: AppBar(), + body: const Center(child: Text("This room isn't available.")), + ); + } + return Scaffold( + appBar: _RoomAppBar(room: room), + body: Column( + children: [ + Expanded(child: _buildTimeline(room)), + if (room.membership == Membership.invite) + _InviteBar(room: room) + else if (room.membership == Membership.join) + room.canSendDefaultMessages + ? _Composer( + controller: _composer, + focus: _focus, + encrypted: room.encrypted, + onSend: _send, + onAttach: _attach, + ) + : const _ReadOnlyBar(), + ], + ), + ); + } + + Widget _buildTimeline(Room room) { + final timeline = _timeline; + if (_loadError != null) { + return Center(child: Text(describeError(_loadError!))); + } + if (timeline == null) { + return const Center(child: CircularProgressIndicator()); + } + final events = timeline.events; + final items = projectTimeline([ + for (final e in events) + (sender: e.senderId, time: e.originServerTs, kind: kindOf(e)), + ]); + final readUpTo = _readByOthersIndex(events, room); + final typing = room.typingUsers + .where((u) => u.id != room.client.userID) + .toList(); + + return ListView.builder( + controller: _scroll, + reverse: true, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: items.length + 2, + itemBuilder: (context, i) { + if (i == 0) return _TypingRow(users: typing); + if (i == items.length + 1) { + return _HistoryEnd( + loading: _loadingHistory || timeline.canRequestHistory, + room: room, + ); + } + final item = items[i - 1]; + return switch (item) { + DayItem(:final day) => _DaySeparator(day: day), + EventItem(:final index, :final kind) when kind == EntryKind.notice => + NoticeRow(event: events[index]), + EventItem(:final index, :final firstInGroup, :final lastInGroup) => + MessageRow( + key: ValueKey(events[index].eventId), + event: events[index], + timeline: timeline, + firstInGroup: firstInGroup, + lastInGroup: lastInGroup, + readByOthers: readUpTo != null && index >= readUpTo, + ), + }; + }, + ); + } + + /// The index (newest first) of the newest event someone else has read, + /// so the user's messages from there back say "Read". + static int? _readByOthersIndex(List events, Room room) { + final me = room.client.userID; + for (var i = 0; i < events.length; i++) { + if (events[i].receipts.any((r) => r.user.id != me)) return i; + } + return null; + } +} + +class _RoomAppBar extends StatelessWidget implements PreferredSizeWidget { + const _RoomAppBar({required this.room}); + final Room room; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + // A room fresh from /createRoom may have no summary yet. + final members = + room.summary.mJoinedMemberCount ?? room.getParticipants().length; + final subtitle = room.isDirectChat + ? room.directChatMatrixID ?? '' + : members == 1 + ? '1 member' + : '$members members'; + return AppBar( + titleSpacing: 0, + title: InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => RoomInfoScreen(roomId: room.id)), + ), + child: Row( + children: [ + Avatar( + name: room.getLocalizedDisplayname(), + seed: room.id, + mxc: room.avatar, + size: 36, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + room.getLocalizedDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.titleMedium, + ), + Row( + children: [ + if (room.encrypted) ...[ + Icon(Icons.lock_outline, size: 12, color: c.tealText), + const SizedBox(width: 3), + ], + Flexible( + child: Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: text.labelSmall, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + actions: [ + IconButton( + tooltip: 'Room info', + icon: const Icon(Icons.info_outline), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => RoomInfoScreen(roomId: room.id)), + ), + ), + ], + ); + } +} + +class _DaySeparator extends StatelessWidget { + const _DaySeparator({required this.day}); + final DateTime day; + + @override + Widget build(BuildContext context) { + final c = context.warp; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Row( + children: [ + Expanded(child: Divider(color: c.line)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + formatDay(day), + style: Theme.of( + context, + ).textTheme.labelMedium?.copyWith(color: c.inkMuted), + ), + ), + Expanded(child: Divider(color: c.line)), + ], + ), + ); + } +} + +class _HistoryEnd extends StatelessWidget { + const _HistoryEnd({required this.loading, required this.room}); + final bool loading; + final Room room; + + @override + Widget build(BuildContext context) { + if (loading) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + final text = Theme.of(context).textTheme; + return Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Column( + children: [ + Avatar( + name: room.getLocalizedDisplayname(), + seed: room.id, + mxc: room.avatar, + size: 64, + ), + const SizedBox(height: 12), + Text( + room.getLocalizedDisplayname(), + style: text.titleLarge, + textAlign: TextAlign.center, + ), + if (room.topic.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + room.topic, + style: text.bodySmall, + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 8), + Text( + 'This is the beginning of the conversation.', + style: text.bodySmall, + ), + ], + ), + ); + } +} + +class _TypingRow extends StatelessWidget { + const _TypingRow({required this.users}); + final List users; + + @override + Widget build(BuildContext context) { + if (users.isEmpty) return const SizedBox(height: 4); + final names = users.map((u) => u.calcDisplayname()).toList(); + final who = switch (names.length) { + 1 => '${names[0]} is typingโ€ฆ', + 2 => '${names[0]} and ${names[1]} are typingโ€ฆ', + _ => 'Several people are typingโ€ฆ', + }; + return Padding( + padding: const EdgeInsets.fromLTRB(60, 4, 16, 8), + child: Text( + who, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: context.warp.inkMuted, + fontStyle: FontStyle.italic, + ), + ), + ); + } +} + +class _Composer extends StatelessWidget { + const _Composer({ + required this.controller, + required this.focus, + required this.encrypted, + required this.onSend, + required this.onAttach, + }); + + final TextEditingController controller; + final FocusNode focus; + final bool encrypted; + final VoidCallback onSend; + final VoidCallback onAttach; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final canSend = controller.text.trim().isNotEmpty; + return Container( + decoration: BoxDecoration( + color: c.surface, + border: Border(top: BorderSide(color: c.line)), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 6, 8, 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton( + tooltip: 'Attach', + icon: const Icon(Icons.add_circle_outline), + color: c.inkMuted, + onPressed: onAttach, + ), + Expanded( + child: TextField( + controller: controller, + focusNode: focus, + minLines: 1, + maxLines: 6, + textCapitalization: TextCapitalization.sentences, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + hintText: encrypted ? 'Encrypted message' : 'Message', + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(22), + borderSide: BorderSide(color: c.tealStrong), + ), + ), + ), + ), + const SizedBox(width: 6), + IconButton.filled( + tooltip: 'Send', + onPressed: canSend ? onSend : null, + icon: const Icon(Icons.send_rounded), + ), + ], + ), + ), + ), + ); + } +} + +class _InviteBar extends StatefulWidget { + const _InviteBar({required this.room}); + final Room room; + + @override + State<_InviteBar> createState() => _InviteBarState(); +} + +class _InviteBarState extends State<_InviteBar> { + bool _busy = false; + + Future _act(bool join) async { + setState(() => _busy = true); + try { + if (join) { + await widget.room.join(); + } else { + await widget.room.leave(); + if (mounted) Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(e)))); + } + } finally { + if (mounted) setState(() => _busy = false); + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "You've been invited to this room.", + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _busy ? null : () => _act(false), + child: const Text('Decline'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: _busy ? null : () => _act(true), + child: const Text('Join'), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _ReadOnlyBar extends StatelessWidget { + const _ReadOnlyBar(); + + @override + Widget build(BuildContext context) => SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + "You can't send messages in this room.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ); +} diff --git a/apps/mobile/lib/screens/sessions_screen.dart b/apps/mobile/lib/screens/sessions_screen.dart new file mode 100644 index 0000000..0c5871b --- /dev/null +++ b/apps/mobile/lib/screens/sessions_screen.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../util/format.dart'; +import '../widgets/verification_sheet.dart'; + +/// The account's sessions: which are verified, verifying one, and signing +/// one out. +class SessionsScreen extends ConsumerStatefulWidget { + const SessionsScreen({super.key}); + + @override + ConsumerState createState() => _SessionsScreenState(); +} + +class _SessionsScreenState extends ConsumerState { + late Future?> _devices; + + @override + void initState() { + super.initState(); + _devices = ref.read(clientProvider).getDevices(); + } + + void _reload() => + setState(() => _devices = ref.read(clientProvider).getDevices()); + + void _snack(Object e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(e)))); + } + + Future _signOut(Device device) async { + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Sign out this session?'), + content: Text(device.displayName ?? device.deviceId), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: context.warp.danger), + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Sign out'), + ), + ], + ), + ); + if (ok != true) return; + final client = ref.read(clientProvider); + try { + // Deleting a device needs the password: the request goes through the + // app's UIA handler. + await client.uiaRequestBackground( + (auth) => client.deleteDevice(device.deviceId, auth: auth), + ); + _reload(); + } catch (e) { + _snack(e); + } + } + + Future _verify(DeviceKeys keys) async { + try { + final request = await keys.startVerification(); + if (mounted) await showVerificationSheet(context, request); + _reload(); + } catch (e) { + _snack(e); + } + } + + @override + Widget build(BuildContext context) { + final client = ref.watch(clientProvider); + final c = context.warp; + return Scaffold( + appBar: AppBar(title: const Text('Sessions')), + body: FutureBuilder?>( + future: _devices, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text(describeError(snapshot.error!))); + } + final devices = snapshot.data; + if (devices == null) { + return const Center(child: CircularProgressIndicator()); + } + devices.sort((a, b) { + if (a.deviceId == client.deviceID) return -1; + if (b.deviceId == client.deviceID) return 1; + return (b.lastSeenTs ?? 0).compareTo(a.lastSeenTs ?? 0); + }); + final ownKeys = client.userDeviceKeys[client.userID]; + return RefreshIndicator( + onRefresh: () async => _reload(), + child: ListView( + children: [ + for (final device in devices) + Builder( + builder: (context) { + final current = device.deviceId == client.deviceID; + final keys = ownKeys?.deviceKeys[device.deviceId]; + final verified = keys?.verified == true; + final seen = device.lastSeenTs; + return ListTile( + leading: Icon( + verified + ? Icons.verified_user + : Icons.gpp_maybe_outlined, + color: verified ? c.tealText : c.warning, + ), + title: Text( + [ + device.displayName ?? device.deviceId, + if (current) '(this session)', + ].join(' '), + ), + subtitle: Text( + [ + device.deviceId, + verified ? 'verified' : 'not verified', + if (seen != null && !current) + 'seen ${formatListTime(DateTime.fromMillisecondsSinceEpoch(seen))}', + ].join(' ยท '), + ), + trailing: current + ? null + : PopupMenuButton( + onSelected: (action) { + if (action == 'verify' && keys != null) { + _verify(keys); + } else if (action == 'signout') { + _signOut(device); + } + }, + itemBuilder: (_) => [ + if (!verified && + keys != null && + client.encryptionEnabled) + const PopupMenuItem( + value: 'verify', + child: Text('Verify'), + ), + const PopupMenuItem( + value: 'signout', + child: Text('Sign out'), + ), + ], + ), + ); + }, + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/apps/mobile/lib/screens/settings_screen.dart b/apps/mobile/lib/screens/settings_screen.dart new file mode 100644 index 0000000..88f64d8 --- /dev/null +++ b/apps/mobile/lib/screens/settings_screen.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../matrix/client.dart'; +import '../matrix/notifications.dart'; +import '../matrix/push.dart'; +import '../state.dart'; +import '../theme.dart'; +import '../widgets/avatar.dart'; +import 'encryption_screen.dart'; +import 'sessions_screen.dart'; + +class SettingsScreen extends ConsumerStatefulWidget { + const SettingsScreen({super.key}); + + @override + ConsumerState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends ConsumerState { + late Future _profile; + late Future _distributor; + bool _busy = false; + + @override + void initState() { + super.initState(); + _profile = ref.read(clientProvider).fetchOwnProfile(); + _distributor = Push.currentDistributor(); + } + + void _snack(String text) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); + } + + Future _run(Future Function() action) async { + setState(() => _busy = true); + try { + await action(); + } catch (e) { + _snack(describeError(e)); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + void _reloadProfile() { + final client = ref.read(clientProvider); + setState( + () => _profile = client.getProfileFromUserId( + client.userID!, + maxCacheAge: Duration.zero, + ), + ); + } + + Future _editName(String current) async { + final controller = TextEditingController(text: current); + final name = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Display name'), + content: TextField( + controller: controller, + autofocus: true, + textCapitalization: TextCapitalization.words, + onSubmitted: (v) => Navigator.pop(dialog, v), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(dialog, controller.text), + child: const Text('Save'), + ), + ], + ), + ); + if (name == null || name.trim().isEmpty) return; + final client = ref.read(clientProvider); + await _run( + () => client.setProfileField(client.userID!, 'displayname', { + 'displayname': name.trim(), + }), + ); + _reloadProfile(); + } + + Future _editAvatar() async { + final picked = await ImagePicker().pickImage( + source: ImageSource.gallery, + maxWidth: 1024, + maxHeight: 1024, + imageQuality: 90, + ); + if (picked == null) return; + final client = ref.read(clientProvider); + final bytes = await picked.readAsBytes(); + await _run( + () => client.setAvatar(MatrixImageFile(bytes: bytes, name: picked.name)), + ); + _reloadProfile(); + } + + Future _togglePush(bool on) async { + if (!on) { + await _run(Push.disable); + setState(() => _distributor = Push.currentDistributor()); + return; + } + await Notifications.requestPermission(); + final options = await Push.distributors(); + if (!mounted) return; + if (options.isEmpty) { + await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Install a push app'), + content: const Text( + 'Warp receives push notifications through UnifiedPush, without ' + 'Google services. Install a distributor such as ntfy, then turn ' + 'this on again.\n\nWithout one, you are notified while Warp is ' + 'open or running in the background.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog), + child: const Text('Not now'), + ), + FilledButton( + onPressed: () { + Navigator.pop(dialog); + launchUrl( + Uri.parse('https://unifiedpush.org/users/distributors/'), + mode: LaunchMode.externalApplication, + ); + }, + child: const Text('Find one'), + ), + ], + ), + ); + return; + } + String? choice = options.length == 1 ? options.single : null; + choice ??= await showDialog( + context: context, + builder: (dialog) => SimpleDialog( + title: const Text('Push through'), + children: [ + for (final d in options) + SimpleDialogOption( + onPressed: () => Navigator.pop(dialog, d), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text(d), + ), + ), + ], + ), + ); + if (choice == null) return; + final distributor = choice; + await _run(() async { + await Push.enable(distributor: distributor); + }); + setState(() => _distributor = Push.currentDistributor()); + } + + Future _signOut() async { + final client = ref.read(clientProvider); + final hasBackup = client.encryption?.keyManager.enabled == true; + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Sign out?'), + content: Text( + hasBackup + ? "This session's keys are deleted from this phone. To read " + 'your encrypted messages after you sign in again, you ' + 'need your recovery key or another verified session.' + : "This session's keys are deleted from this phone, and " + "encryption isn't set up for your account: encrypted " + "messages only this session can read will be lost.", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: context.warp.danger), + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Sign out'), + ), + ], + ), + ); + if (ok != true) return; + await _run(() async { + await Push.forget(); + await Notifications.clearAll(); + try { + await client.logout(); + } catch (e) { + // The session may already be gone on the server; sign out here + // regardless. + Logs().w('Logout request failed, clearing locally', e); + await client.clear(); + } + }); + } + + @override + Widget build(BuildContext context) { + final client = ref.watch(clientProvider); + final themeMode = ref.watch(themeModeProvider); + final c = context.warp; + final text = Theme.of(context).textTheme; + + return Scaffold( + appBar: AppBar(title: const Text('Settings')), + body: AbsorbPointer( + absorbing: _busy, + child: ListView( + children: [ + if (_busy) const LinearProgressIndicator(minHeight: 2), + FutureBuilder( + future: _profile, + builder: (context, snapshot) { + final profile = snapshot.data; + final name = profile?.displayName ?? client.userID!.localpart!; + return Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + GestureDetector( + onTap: _editAvatar, + child: Stack( + children: [ + Avatar( + name: name, + seed: client.userID!, + mxc: profile?.avatarUrl, + size: 72, + ), + Positioned( + right: 0, + bottom: 0, + child: CircleAvatar( + radius: 12, + backgroundColor: c.tealStrong, + child: Icon( + Icons.photo_camera, + size: 14, + color: c.onTeal, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + name, + style: text.titleLarge, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + tooltip: 'Change display name', + icon: const Icon( + Icons.edit_outlined, + size: 18, + ), + onPressed: () => _editName(name), + ), + ], + ), + Text( + client.userID!, + style: text.bodySmall?.copyWith( + fontFamily: fontMono, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + const Divider(), + _Header('Security'), + ListTile( + leading: const Icon(Icons.key_outlined), + title: const Text('Encryption'), + subtitle: Text(_encryptionSummary(client)), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const EncryptionScreen()), + ), + ), + ListTile( + leading: const Icon(Icons.devices_outlined), + title: const Text('Sessions'), + subtitle: const Text('Where you are signed in'), + onTap: () => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const SessionsScreen())), + ), + const Divider(), + _Header('Notifications'), + FutureBuilder( + future: _distributor, + builder: (context, snapshot) { + final distributor = snapshot.data; + return SwitchListTile( + secondary: const Icon(Icons.notifications_active_outlined), + title: const Text('Push notifications'), + subtitle: Text( + distributor == null + ? 'Off: notified only while Warp runs' + : 'Through $distributor', + ), + value: distributor != null, + onChanged: _togglePush, + ); + }, + ), + const Divider(), + _Header('Appearance'), + RadioGroup( + groupValue: themeMode, + onChanged: (m) { + if (m != null) ref.read(themeModeProvider.notifier).set(m); + }, + child: Column( + children: [ + for (final (mode, label) in [ + (ThemeMode.system, 'System'), + (ThemeMode.light, 'Light'), + (ThemeMode.dark, 'Dark'), + ]) + RadioListTile(value: mode, title: Text(label)), + ], + ), + ), + const Divider(), + _Header('About'), + ListTile( + leading: const Icon(Icons.dns_outlined), + title: const Text('Homeserver'), + subtitle: Text(client.homeserver?.toString() ?? ''), + ), + ListTile( + leading: const Icon(Icons.smartphone), + title: const Text('This session'), + subtitle: Text( + '${client.deviceName ?? deviceDisplayName} ยท ' + '${client.deviceID}', + ), + ), + const Divider(), + ListTile( + leading: Icon(Icons.logout, color: c.danger), + title: Text('Sign out', style: TextStyle(color: c.danger)), + onTap: _signOut, + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + static String _encryptionSummary(Client client) { + final encryption = client.encryption; + if (encryption == null) return 'Not available'; + if (!encryption.crossSigning.enabled) return 'Not set up'; + if (client.isUnknownSession) return 'This session is not verified'; + return 'Set up ยท this session is verified'; + } +} + +class _Header extends StatelessWidget { + const _Header(this.text); + final String text; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Text( + text, + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(color: context.warp.tealText), + ), + ); +} diff --git a/apps/mobile/lib/state.dart b/apps/mobile/lib/state.dart new file mode 100644 index 0000000..3553767 --- /dev/null +++ b/apps/mobile/lib/state.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:matrix/matrix.dart'; + +/// The one Matrix client, created in main() before the app starts. +final clientProvider = Provider( + (ref) => throw UnimplementedError('overridden in main()'), +); + +/// Signed in or not. Starts from the restored session. +final loginStateProvider = StreamProvider((ref) async* { + final client = ref.watch(clientProvider); + yield client.isLogged() ? LoginState.loggedIn : LoginState.loggedOut; + yield* client.onLoginStateChanged.stream; +}); + +/// Bumps on every sync. The SDK's objects change in place, so screens that +/// show rooms watch this and read the client again; that is the whole +/// trick that keeps them current. +final syncProvider = StreamProvider((ref) async* { + final client = ref.watch(clientProvider); + var n = 0; + yield n; + await for (final _ in client.onSync.stream) { + yield ++n; + } +}); + +/// Whether the connection to the homeserver is working, from the sync +/// loop's status. Null until the first sync after start finishes. +final syncErrorProvider = StreamProvider((ref) async* { + final client = ref.watch(clientProvider); + await for (final status in client.onSyncStatus.stream) { + if (status.status == SyncStatus.error) yield true; + if (status.status == SyncStatus.finished) yield false; + } +}); + +/// The theme the user picked: the system's, light or dark. +class ThemeModeNotifier extends Notifier { + static const _key = 'warp.theme'; + static const _storage = FlutterSecureStorage(); + + @override + ThemeMode build() { + _storage.read(key: _key).then((value) { + final mode = ThemeMode.values.where((m) => m.name == value).firstOrNull; + if (mode != null) state = mode; + }); + return ThemeMode.system; + } + + void set(ThemeMode mode) { + state = mode; + _storage.write(key: _key, value: mode.name); + } +} + +final themeModeProvider = NotifierProvider( + ThemeModeNotifier.new, +); diff --git a/apps/mobile/lib/theme.dart b/apps/mobile/lib/theme.dart new file mode 100644 index 0000000..aea3d87 --- /dev/null +++ b/apps/mobile/lib/theme.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; + +/// Warp's design tokens (apps/ui/src/styles/tokens.css) as a Flutter theme. +/// +/// The token names are kept so a colour can be traced back to the brand +/// package: [WarpColors.light] and [WarpColors.dark] are `:root` and +/// `[data-theme="dark"]`. +@immutable +class WarpColors extends ThemeExtension { + const WarpColors({ + required this.surface, + required this.surfaceSunken, + required this.surfaceRaised, + required this.line, + required this.lineStrong, + required this.ink, + required this.inkMuted, + required this.teal, + required this.tealStrong, + required this.onTeal, + required this.tealText, + required this.tealSoft, + required this.bubbleOther, + required this.danger, + required this.dangerSoft, + required this.warning, + required this.warningSoft, + required this.info, + required this.infoSoft, + required this.presenceOnline, + required this.presenceAway, + required this.presenceOffline, + }); + + final Color surface; + final Color surfaceSunken; + final Color surfaceRaised; + final Color line; + final Color lineStrong; + final Color ink; + final Color inkMuted; + final Color teal; + final Color tealStrong; + final Color onTeal; + final Color tealText; + final Color tealSoft; + final Color bubbleOther; + final Color danger; + final Color dangerSoft; + final Color warning; + final Color warningSoft; + final Color info; + final Color infoSoft; + final Color presenceOnline; + final Color presenceAway; + final Color presenceOffline; + + static const light = WarpColors( + surface: Color(0xFFFFFFFF), + surfaceSunken: Color(0xFFF3F5F4), + surfaceRaised: Color(0xFFFFFFFF), + line: Color(0xFFE1E6E4), + lineStrong: Color(0xFF7B8682), + ink: Color(0xFF111114), + inkMuted: Color(0xFF5B6461), + teal: Color(0xFF00A389), + tealStrong: Color(0xFF00806A), + onTeal: Color(0xFFFFFFFF), + tealText: Color(0xFF00735F), + tealSoft: Color(0xFFDDF4EE), + bubbleOther: Color(0xFFEFF2F1), + danger: Color(0xFFB42318), + dangerSoft: Color(0xFFFDE8E6), + warning: Color(0xFF8A5300), + warningSoft: Color(0xFFFFF2DB), + info: Color(0xFF1D5FD1), + infoSoft: Color(0xFFE3EDFF), + presenceOnline: Color(0xFF00A389), + presenceAway: Color(0xFFD99A00), + presenceOffline: Color(0xFF8A9490), + ); + + static const dark = WarpColors( + surface: Color(0xFF0F1213), + surfaceSunken: Color(0xFF161A1C), + surfaceRaised: Color(0xFF1B2022), + line: Color(0xFF2A3134), + lineStrong: Color(0xFF6E7976), + ink: Color(0xFFECEEEE), + inkMuted: Color(0xFF9AA5A1), + teal: Color(0xFF22C3A6), + tealStrong: Color(0xFF22C3A6), + onTeal: Color(0xFF062A23), + tealText: Color(0xFF3ED6B8), + tealSoft: Color(0xFF123A33), + bubbleOther: Color(0xFF1E2427), + danger: Color(0xFFF97066), + dangerSoft: Color(0xFF3A1512), + warning: Color(0xFFF5B547), + warningSoft: Color(0xFF33260B), + info: Color(0xFF7EB1FF), + infoSoft: Color(0xFF14233F), + presenceOnline: Color(0xFF22C3A6), + presenceAway: Color(0xFFF5B547), + presenceOffline: Color(0xFF6E7976), + ); + + @override + WarpColors copyWith() => this; + + @override + WarpColors lerp(WarpColors? other, double t) => + other == null || t < 0.5 ? this : other; +} + +extension WarpTheme on BuildContext { + WarpColors get warp => Theme.of(this).extension()!; +} + +const fontDisplay = 'Outfit'; +const fontSans = 'Figtree'; +const fontMono = 'JetBrains Mono'; + +ThemeData warpTheme(Brightness brightness) { + final c = brightness == Brightness.dark ? WarpColors.dark : WarpColors.light; + final scheme = ColorScheme( + brightness: brightness, + primary: c.tealStrong, + onPrimary: c.onTeal, + primaryContainer: c.tealSoft, + onPrimaryContainer: c.tealText, + secondary: c.teal, + onSecondary: c.onTeal, + error: c.danger, + onError: brightness == Brightness.dark ? c.surface : Colors.white, + errorContainer: c.dangerSoft, + onErrorContainer: c.danger, + surface: c.surface, + onSurface: c.ink, + onSurfaceVariant: c.inkMuted, + surfaceContainerLowest: c.surface, + surfaceContainerLow: c.surfaceSunken, + surfaceContainer: c.surfaceSunken, + surfaceContainerHigh: c.surfaceRaised, + surfaceContainerHighest: c.bubbleOther, + outline: c.lineStrong, + outlineVariant: c.line, + ); + + TextStyle display(double size, double height, FontWeight w) => TextStyle( + fontFamily: fontDisplay, + fontSize: size, + height: height / size, + fontWeight: w, + color: c.ink, + ); + TextStyle sans(double size, double height, FontWeight w, [Color? color]) => + TextStyle( + fontFamily: fontSans, + fontSize: size, + height: height / size, + fontWeight: w, + color: color ?? c.ink, + ); + + final text = TextTheme( + displaySmall: display(40, 44, FontWeight.w700), + headlineMedium: display(28, 34, FontWeight.w600), + headlineSmall: display(24, 30, FontWeight.w600), + titleLarge: display(20, 26, FontWeight.w600), + titleMedium: display(16, 22, FontWeight.w600), + titleSmall: sans(14, 20, FontWeight.w600), + bodyLarge: sans(15, 22, FontWeight.w400), + bodyMedium: sans(14, 20, FontWeight.w400), + bodySmall: sans(13, 18, FontWeight.w400, c.inkMuted), + labelLarge: sans(14, 20, FontWeight.w600), + labelMedium: sans(12, 16, FontWeight.w500), + labelSmall: sans(11, 14, FontWeight.w500, c.inkMuted), + ); + + final radius = BorderRadius.circular(12); + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: scheme, + fontFamily: fontSans, + textTheme: text, + scaffoldBackgroundColor: c.surface, + extensions: [c], + dividerTheme: DividerThemeData(color: c.line, space: 1, thickness: 1), + appBarTheme: AppBarTheme( + backgroundColor: c.surface, + foregroundColor: c.ink, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + titleTextStyle: display(18, 24, FontWeight.w600), + shape: Border(bottom: BorderSide(color: c.line)), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: c.surfaceSunken, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: c.line), + ), + enabledBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: c.line), + ), + focusedBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: c.tealStrong, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: c.danger), + ), + labelStyle: sans(14, 20, FontWeight.w500, c.inkMuted), + hintStyle: sans(14, 20, FontWeight.w400, c.inkMuted), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: c.tealStrong, + foregroundColor: c.onTeal, + minimumSize: const Size(64, 44), + textStyle: sans(14, 20, FontWeight.w600), + shape: RoundedRectangleBorder(borderRadius: radius), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: c.ink, + minimumSize: const Size(64, 44), + side: BorderSide(color: c.line), + textStyle: sans(14, 20, FontWeight.w600), + shape: RoundedRectangleBorder(borderRadius: radius), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: c.tealText, + textStyle: sans(14, 20, FontWeight.w600), + ), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: c.tealStrong, + foregroundColor: c.onTeal, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + ), + dialogTheme: DialogThemeData( + backgroundColor: c.surfaceRaised, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + titleTextStyle: display(20, 26, FontWeight.w600), + ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: c.surfaceRaised, + surfaceTintColor: Colors.transparent, + showDragHandle: true, + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: c.ink, + contentTextStyle: sans(14, 20, FontWeight.w500, c.surface), + shape: RoundedRectangleBorder(borderRadius: radius), + ), + listTileTheme: ListTileThemeData( + iconColor: c.inkMuted, + titleTextStyle: sans(15, 22, FontWeight.w500), + subtitleTextStyle: sans(13, 18, FontWeight.w400, c.inkMuted), + ), + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith( + (s) => s.contains(WidgetState.selected) ? c.onTeal : c.lineStrong, + ), + trackColor: WidgetStateProperty.resolveWith( + (s) => s.contains(WidgetState.selected) ? c.tealStrong : c.line, + ), + ), + progressIndicatorTheme: ProgressIndicatorThemeData(color: c.teal), + ); +} diff --git a/apps/mobile/lib/util/format.dart b/apps/mobile/lib/util/format.dart new file mode 100644 index 0000000..9702885 --- /dev/null +++ b/apps/mobile/lib/util/format.dart @@ -0,0 +1,45 @@ +import 'package:intl/intl.dart'; + +/// The time next to a room in the list: the time today, the weekday this +/// week, the date before that. +String formatListTime(DateTime time, {DateTime? now}) { + final local = time.toLocal(); + final today = _day(now ?? DateTime.now()); + final day = _day(local); + final days = today.difference(day).inDays; + if (days <= 0) return DateFormat.Hm().format(local); + if (days == 1) return 'Yesterday'; + if (days < 7) return DateFormat.E().format(local); + if (local.year == today.year) return DateFormat.MMMd().format(local); + return DateFormat.yMd().format(local); +} + +/// The separator above a day's messages. +String formatDay(DateTime day, {DateTime? now}) { + final today = _day(now ?? DateTime.now()); + final days = today.difference(_day(day)).inDays; + if (days == 0) return 'Today'; + if (days == 1) return 'Yesterday'; + if (days < 7) return DateFormat.EEEE().format(day); + if (day.year == today.year) return DateFormat.MMMMEEEEd().format(day); + return DateFormat.yMMMMEEEEd().format(day); +} + +/// The time under a message. +String formatTime(DateTime time) => DateFormat.Hm().format(time.toLocal()); + +/// A file size for people: 980 B, 12 KB, 3.4 MB. +String formatBytes(int bytes) { + if (bytes < 1000) return '$bytes B'; + const units = ['KB', 'MB', 'GB']; + var value = bytes / 1000; + var unit = 0; + while (value >= 1000 && unit < units.length - 1) { + value /= 1000; + unit++; + } + final digits = value < 10 ? 1 : 0; + return '${value.toStringAsFixed(digits)} ${units[unit]}'; +} + +DateTime _day(DateTime t) => DateTime(t.year, t.month, t.day); diff --git a/apps/mobile/lib/widgets/avatar.dart b/apps/mobile/lib/widgets/avatar.dart new file mode 100644 index 0000000..6a71aa0 --- /dev/null +++ b/apps/mobile/lib/widgets/avatar.dart @@ -0,0 +1,115 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../matrix/media.dart'; +import '../state.dart'; +import '../theme.dart'; + +/// A round avatar: the picture at [mxc] when there is one, else the first +/// letter of [name] on a colour picked from [seed] (the user or room ID), so +/// the same person always gets the same colour. +class Avatar extends ConsumerStatefulWidget { + const Avatar({ + super.key, + required this.name, + required this.seed, + this.mxc, + this.size = 40, + }); + + final String name; + final String seed; + final Uri? mxc; + final double size; + + @override + ConsumerState createState() => _AvatarState(); +} + +class _AvatarState extends ConsumerState { + Future? _bytes; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void didUpdateWidget(Avatar old) { + super.didUpdateWidget(old); + if (old.mxc != widget.mxc || old.size != widget.size) _load(); + } + + void _load() { + final mxc = widget.mxc; + _bytes = mxc == null || mxc.scheme != 'mxc' + ? null + : Media.thumbnail(ref.read(clientProvider), mxc, widget.size.round()); + } + + static const _palette = [ + Color(0xFF00A389), + Color(0xFF1D5FD1), + Color(0xFF8A5300), + Color(0xFFB42318), + Color(0xFF6D4AC2), + Color(0xFF00806A), + Color(0xFFC2410C), + Color(0xFF0E7490), + ]; + + @override + Widget build(BuildContext context) { + final size = widget.size; + final hash = widget.seed.codeUnits.fold( + 0, + (h, c) => (h * 31 + c) & 0x7fffffff, + ); + final colour = _palette[hash.abs() % _palette.length]; + final letter = _initial(widget.name); + final fallback = Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration(color: colour, shape: BoxShape.circle), + child: Text( + letter, + style: TextStyle( + fontFamily: fontDisplay, + fontWeight: FontWeight.w600, + fontSize: size * 0.42, + color: Colors.white, + height: 1, + ), + ), + ); + final bytes = _bytes; + if (bytes == null) return fallback; + return FutureBuilder( + future: bytes, + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) return fallback; + return ClipOval( + child: Image.memory( + data, + width: size, + height: size, + fit: BoxFit.cover, + gaplessPlayback: true, + errorBuilder: (_, _, _) => fallback, + ), + ); + }, + ); + } + + static String _initial(String name) { + final trimmed = name.replaceFirst(RegExp(r'^[@#!+]'), '').trim(); + if (trimmed.isEmpty) return '?'; + return String.fromCharCode(trimmed.runes.first).toUpperCase(); + } +} diff --git a/apps/mobile/lib/widgets/message.dart b/apps/mobile/lib/widgets/message.dart new file mode 100644 index 0000000..4d9fdc7 --- /dev/null +++ b/apps/mobile/lib/widgets/message.dart @@ -0,0 +1,694 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../matrix/client.dart'; +import '../matrix/media.dart'; +import '../theme.dart'; +import '../util/format.dart'; +import 'avatar.dart'; + +const _i18n = MatrixDefaultLocalizations(); + +/// A one-line room event in the middle of the timeline: "Alice joined". +class NoticeRow extends StatelessWidget { + const NoticeRow({super.key, required this.event}); + final Event event; + + @override + Widget build(BuildContext context) { + final c = context.warp; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 6), + child: Text( + event.calcLocalizedBodyFallback(_i18n), + textAlign: TextAlign.center, + style: Theme.of( + context, + ).textTheme.labelMedium?.copyWith(color: c.inkMuted), + ), + ); + } +} + +/// A message: a bubble on the right for the user's own, on the left with +/// the sender's avatar and name for everyone else's. +class MessageRow extends StatelessWidget { + const MessageRow({ + super.key, + required this.event, + required this.timeline, + required this.firstInGroup, + required this.lastInGroup, + required this.readByOthers, + }); + + final Event event; + final Timeline timeline; + final bool firstInGroup; + final bool lastInGroup; + final bool readByOthers; + + bool get _mine => event.senderId == event.room.client.userID; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + final display = event.getDisplayEvent(timeline); + final sender = event.senderFromMemoryOrFallback; + final showSender = !_mine && firstInGroup && !event.room.isDirectChat; + final bubbleColour = _mine ? c.tealSoft : c.bubbleOther; + final isMedia = + !display.redacted && + (display.messageType == MessageTypes.Image || + display.messageType == MessageTypes.Sticker); + + final radius = Radius.circular(18); + final tight = Radius.circular(6); + final shape = BorderRadius.only( + topLeft: !_mine && !firstInGroup ? tight : radius, + bottomLeft: !_mine && !lastInGroup ? tight : radius, + topRight: _mine && !firstInGroup ? tight : radius, + bottomRight: _mine && !lastInGroup ? tight : radius, + ); + + final body = _MessageBody(event: display, original: event, mine: _mine); + final bubble = GestureDetector( + onLongPress: () => _showActions(context, display), + child: isMedia + ? ClipRRect(borderRadius: shape, child: body) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), + decoration: BoxDecoration( + color: bubbleColour, + borderRadius: shape, + ), + child: body, + ), + ); + + return Padding( + padding: EdgeInsets.fromLTRB( + _mine ? 56 : 12, + firstInGroup ? 8 : 2, + _mine ? 12 : 56, + lastInGroup ? 4 : 0, + ), + child: Row( + mainAxisAlignment: _mine + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!_mine) ...[ + SizedBox( + width: 32, + child: lastInGroup + ? Avatar( + name: sender.calcDisplayname(), + seed: sender.id, + mxc: sender.avatarUrl, + size: 32, + ) + : null, + ), + const SizedBox(width: 8), + ], + Flexible( + child: Column( + crossAxisAlignment: _mine + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (showSender) + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 2), + child: Text( + sender.calcDisplayname(), + style: text.labelMedium?.copyWith( + color: c.tealText, + fontWeight: FontWeight.w600, + ), + ), + ), + bubble, + if (lastInGroup || event.status.isError) + Padding( + padding: const EdgeInsets.only(top: 3, left: 4, right: 4), + child: _Status( + event: event, + edited: + display != event || + event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + ), + mine: _mine, + read: readByOthers, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _showActions(BuildContext context, Event display) async { + final hasText = + !display.redacted && + display.type == EventTypes.Message && + !display.hasAttachment; + final canSave = !display.redacted && display.hasAttachment; + final canDelete = + !event.redacted && + event.status.isSent && + event.canRedact && + event.room.canRedact; + final failed = event.status.isError; + + final action = await showModalBottomSheet( + context: context, + builder: (sheet) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (failed) ...[ + ListTile( + leading: const Icon(Icons.refresh), + title: const Text('Retry'), + onTap: () => Navigator.pop(sheet, 'retry'), + ), + ListTile( + leading: const Icon(Icons.delete_outline), + title: const Text('Delete'), + onTap: () => Navigator.pop(sheet, 'cancel'), + ), + ], + if (hasText) + ListTile( + leading: const Icon(Icons.copy), + title: const Text('Copy text'), + onTap: () => Navigator.pop(sheet, 'copy'), + ), + if (canSave) + ListTile( + leading: const Icon(Icons.download_outlined), + title: const Text('Save'), + onTap: () => Navigator.pop(sheet, 'save'), + ), + if (canDelete) + ListTile( + leading: Icon(Icons.delete_outline, color: context.warp.danger), + title: Text( + 'Delete', + style: TextStyle(color: context.warp.danger), + ), + onTap: () => Navigator.pop(sheet, 'redact'), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + if (!context.mounted) return; + try { + switch (action) { + case 'retry': + await event.sendAgain(); + case 'cancel': + await event.cancelSend(); + case 'copy': + await Clipboard.setData( + ClipboardData( + text: display.calcLocalizedBodyFallback( + _i18n, + hideReply: true, + plaintextBody: false, + ), + ), + ); + case 'save': + await saveAttachment(context, display); + case 'redact': + final ok = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Delete message?'), + content: const Text( + 'It is removed for everyone in the room. This can\'t be ' + 'undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialog, false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: context.warp.danger, + ), + onPressed: () => Navigator.pop(dialog, true), + child: const Text('Delete'), + ), + ], + ), + ); + if (ok == true) await event.redactEvent(); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(describeError(e)))); + } + } + } +} + +/// Under a group: the time, "edited", and for the user's own messages +/// whether it is sending, sent, read, or failed. +class _Status extends StatelessWidget { + const _Status({ + required this.event, + required this.edited, + required this.mine, + required this.read, + }); + + final Event event; + final bool edited; + final bool mine; + final bool read; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final style = Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: c.inkMuted); + final status = event.status; + final parts = [ + TextSpan(text: formatTime(event.originServerTs)), + if (edited) const TextSpan(text: ' ยท edited'), + ]; + if (mine) { + if (status.isError) { + parts.add( + TextSpan( + text: ' ยท Not sent. Hold for Retry', + style: style?.copyWith(color: c.danger), + ), + ); + } else if (status.isSending) { + parts.add(const TextSpan(text: ' ยท Sending')); + } else if (read) { + parts.add( + TextSpan( + text: ' ยท Read', + style: style?.copyWith(color: c.tealText), + ), + ); + } else { + parts.add(const TextSpan(text: ' ยท Sent')); + } + } + return Text.rich(TextSpan(children: parts), style: style); + } +} + +class _MessageBody extends StatelessWidget { + const _MessageBody({ + required this.event, + required this.original, + required this.mine, + }); + + /// The event as shown: the latest edit of [original]. + final Event event; + final Event original; + final bool mine; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + final muted = text.bodyLarge?.copyWith( + color: c.inkMuted, + fontStyle: FontStyle.italic, + ); + + if (event.redacted) { + return Text('Message deleted', style: muted); + } + if (event.type == EventTypes.Encrypted) { + return _UndecryptableBody(event: event); + } + + switch (event.messageType) { + case MessageTypes.Image: + case MessageTypes.Sticker: + return _ImageBody(event: event); + case MessageTypes.File: + case MessageTypes.Video: + case MessageTypes.Audio: + return _FileBody(event: event); + case MessageTypes.BadEncrypted: + return _UndecryptableBody(event: event); + } + + final body = event.calcLocalizedBodyFallback( + _i18n, + hideReply: true, + plaintextBody: false, + ); + final style = event.messageType == MessageTypes.Emote + ? text.bodyLarge?.copyWith(fontStyle: FontStyle.italic) + : event.messageType == MessageTypes.Notice + ? text.bodyLarge?.copyWith(color: c.inkMuted) + : text.bodyLarge; + final emote = event.messageType == MessageTypes.Emote + ? '* ${event.senderFromMemoryOrFallback.calcDisplayname()} ' + : ''; + return LinkifiedText('$emote$body', style: style); + } +} + +/// A message this session has no key for: why, in words, rather than just +/// "encrypted". +class _UndecryptableBody extends StatelessWidget { + const _UndecryptableBody({required this.event}); + final Event event; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final client = event.room.client; + final String reason; + if (!client.encryptionEnabled) { + reason = "Encrypted. This app can't decrypt messages."; + } else if (client.isUnknownSession) { + reason = + "Can't decrypt this yet. Verify this session in Settings to read " + 'older messages.'; + } else if (event.content['can_request_session'] == true) { + reason = + 'Waiting for the key to this message. It may have been sent ' + 'before this session signed in.'; + } else { + reason = "Couldn't decrypt this message."; + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.lock_clock_outlined, size: 16, color: c.inkMuted), + const SizedBox(width: 6), + Flexible( + child: Text( + reason, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: c.inkMuted, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ); + } +} + +class _ImageBody extends StatefulWidget { + const _ImageBody({required this.event}); + final Event event; + + @override + State<_ImageBody> createState() => _ImageBodyState(); +} + +class _ImageBodyState extends State<_ImageBody> { + late Future _bytes = Media.attachment( + widget.event, + thumbnail: true, + ); + + @override + Widget build(BuildContext context) { + final c = context.warp; + final info = widget.event.infoMap; + final w = (info['w'] as num?)?.toDouble(); + final h = (info['h'] as num?)?.toDouble(); + final maxWidth = MediaQuery.sizeOf(context).width * 0.66; + var width = maxWidth.clamp(120.0, 320.0); + var height = width * 0.75; + if (w != null && h != null && w > 0 && h > 0) { + height = (width * h / w).clamp(80.0, 360.0); + if (h > w) width = (height * w / h).clamp(80.0, width); + } + + return FutureBuilder( + future: _bytes, + builder: (context, snapshot) { + final data = snapshot.data; + final placeholder = Container( + width: width, + height: height, + color: c.bubbleOther, + alignment: Alignment.center, + child: snapshot.connectionState == ConnectionState.done + ? IconButton( + tooltip: 'Retry', + icon: Icon(Icons.broken_image_outlined, color: c.inkMuted), + onPressed: () => setState(() { + Media.evict(widget.event); + _bytes = Media.attachment(widget.event, thumbnail: true); + }), + ) + : const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + if (data == null) return placeholder; + return GestureDetector( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ImageViewer(event: widget.event)), + ), + child: Image.memory( + data, + width: width, + height: height, + fit: BoxFit.cover, + gaplessPlayback: true, + errorBuilder: (_, _, _) => placeholder, + ), + ); + }, + ); + } +} + +class _FileBody extends StatefulWidget { + const _FileBody({required this.event}); + final Event event; + + @override + State<_FileBody> createState() => _FileBodyState(); +} + +class _FileBodyState extends State<_FileBody> { + bool _saving = false; + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + final event = widget.event; + final size = event.infoMap['size']; + final icon = switch (event.messageType) { + MessageTypes.Video => Icons.movie_outlined, + MessageTypes.Audio => Icons.audiotrack_outlined, + _ => Icons.insert_drive_file_outlined, + }; + return InkWell( + onTap: _saving + ? null + : () async { + setState(() => _saving = true); + try { + await saveAttachment(context, event); + } finally { + if (mounted) setState(() => _saving = false); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: c.surface, + borderRadius: BorderRadius.circular(10), + ), + child: _saving + ? const Padding( + padding: EdgeInsets.all(10), + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon(icon, color: c.tealText), + ), + const SizedBox(width: 10), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.body, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: text.titleSmall, + ), + Text( + [ + if (size is int) formatBytes(size), + 'Tap to save', + ].join(' ยท '), + style: text.labelSmall, + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Downloads (and decrypts) an attachment and asks where to save it. +Future saveAttachment(BuildContext context, Event event) async { + final messenger = ScaffoldMessenger.of(context); + try { + final file = await event.downloadAndDecryptAttachment(); + final saved = await FilePicker.saveFile( + fileName: file.name, + bytes: file.bytes, + ); + if (saved != null) { + messenger.showSnackBar(SnackBar(content: Text('Saved ${file.name}'))); + } + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text(describeError(e)))); + } +} + +/// An image full screen, pinch to zoom. +class ImageViewer extends StatelessWidget { + const ImageViewer({super.key, required this.event}); + final Event event; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: const Border(), + title: Text( + event.body, + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + actions: [ + IconButton( + tooltip: 'Save', + icon: const Icon(Icons.download_outlined), + onPressed: () => saveAttachment(context, event), + ), + ], + ), + body: FutureBuilder( + future: Media.attachment(event), + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) { + return Center( + child: snapshot.connectionState == ConnectionState.done + ? const Text( + "Couldn't load the image.", + style: TextStyle(color: Colors.white), + ) + : const CircularProgressIndicator(), + ); + } + return InteractiveViewer( + maxScale: 6, + child: Center(child: Image.memory(data)), + ); + }, + ), + ); + } +} + +/// Text with web links that open in the browser. +class LinkifiedText extends StatefulWidget { + const LinkifiedText(this.text, {super.key, this.style}); + final String text; + final TextStyle? style; + + static final _url = RegExp( + r'''\bhttps?://[^\s<>"']+[^\s<>"'.,;:!?)\]]''', + caseSensitive: false, + ); + + @override + State createState() => _LinkifiedTextState(); +} + +class _LinkifiedTextState extends State { + final _recognizers = []; + + @override + void dispose() { + for (final r in _recognizers) { + r.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + for (final r in _recognizers) { + r.dispose(); + } + _recognizers.clear(); + final text = widget.text; + final matches = LinkifiedText._url.allMatches(text).toList(); + if (matches.isEmpty) return Text(text, style: widget.style); + + final link = widget.style?.copyWith( + color: context.warp.tealText, + decoration: TextDecoration.underline, + decorationColor: context.warp.tealText, + ); + final spans = []; + var at = 0; + for (final m in matches) { + if (m.start > at) spans.add(TextSpan(text: text.substring(at, m.start))); + final url = m.group(0)!; + final recognizer = TapGestureRecognizer() + ..onTap = () => + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + _recognizers.add(recognizer); + spans.add(TextSpan(text: url, style: link, recognizer: recognizer)); + at = m.end; + } + if (at < text.length) spans.add(TextSpan(text: text.substring(at))); + return Text.rich(TextSpan(children: spans), style: widget.style); + } +} diff --git a/apps/mobile/lib/widgets/new_chat_sheet.dart b/apps/mobile/lib/widgets/new_chat_sheet.dart new file mode 100644 index 0000000..6b7e6c8 --- /dev/null +++ b/apps/mobile/lib/widgets/new_chat_sheet.dart @@ -0,0 +1,329 @@ +import 'package:flutter/material.dart' hide Visibility; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:matrix/matrix.dart'; + +import '../matrix/client.dart'; +import '../screens/room_screen.dart'; +import '../state.dart'; + +/// Start a direct chat, create a room or join one. +Future showNewChatSheet(BuildContext context) => + showModalBottomSheet( + context: context, + builder: (sheet) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.person_add_alt_1_outlined), + title: const Text('Message someone'), + subtitle: const Text('A direct chat, end-to-end encrypted'), + onTap: () { + Navigator.pop(sheet); + _open(context, const _StartChatDialog()); + }, + ), + ListTile( + leading: const Icon(Icons.group_add_outlined), + title: const Text('New room'), + subtitle: const Text('For a group or a topic'), + onTap: () { + Navigator.pop(sheet); + _open(context, const _CreateRoomDialog()); + }, + ), + ListTile( + leading: const Icon(Icons.login), + title: const Text('Join a room'), + subtitle: const Text('By its address, like #room:example.org'), + onTap: () { + Navigator.pop(sheet); + _open(context, const _JoinRoomDialog()); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + +Future _open(BuildContext context, Widget dialog) async { + final roomId = await showDialog( + context: context, + builder: (_) => dialog, + ); + if (roomId != null && context.mounted) { + Navigator.of(context).push(RoomScreen.route(roomId)); + } +} + +/// A dialog with one form and a busy state, returning a room ID. +class _FormDialog extends StatefulWidget { + const _FormDialog({ + required this.title, + required this.action, + required this.fields, + required this.submit, + }); + + final String title; + final String action; + final List fields; + final Future Function() submit; + + @override + State<_FormDialog> createState() => _FormDialogState(); +} + +class _FormDialogState extends State<_FormDialog> { + bool _busy = false; + String? _error; + + Future _go() async { + setState(() { + _busy = true; + _error = null; + }); + try { + final roomId = await widget.submit(); + if (mounted) Navigator.pop(context, roomId); + } catch (e) { + if (mounted) { + setState(() { + _busy = false; + _error = describeError(e); + }); + } + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.title), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...widget.fields, + if (_error != null) ...[ + const SizedBox(height: 12), + Text( + _error!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: _busy ? null : () => Navigator.pop(context), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: _busy ? null : _go, + child: _busy + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(widget.action), + ), + ], + ); + } +} + +class _StartChatDialog extends ConsumerStatefulWidget { + const _StartChatDialog(); + + @override + ConsumerState<_StartChatDialog> createState() => _StartChatDialogState(); +} + +class _StartChatDialogState extends ConsumerState<_StartChatDialog> { + final _userId = TextEditingController(); + + @override + void dispose() { + _userId.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final client = ref.watch(clientProvider); + return _FormDialog( + title: 'Message someone', + action: 'Start chat', + fields: [ + TextField( + controller: _userId, + autofocus: true, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'User ID', + hintText: '@alice:${client.userID?.domain ?? 'example.org'}', + ), + ), + ], + submit: () async { + var id = _userId.text.trim(); + if (!id.startsWith('@')) id = '@$id'; + if (!id.contains(':')) id = '$id:${client.userID!.domain}'; + if (!id.isValidMatrixIdStrict()) { + throw const FormatException("That isn't a user ID."); + } + if (id == client.userID) { + throw const FormatException("That's you."); + } + // Check they exist before creating a room for nobody. + await client.getUserProfile(id); + return client.startDirectChat( + id, + enableEncryption: client.encryptionEnabled, + ); + }, + ); + } +} + +class _CreateRoomDialog extends ConsumerStatefulWidget { + const _CreateRoomDialog(); + + @override + ConsumerState<_CreateRoomDialog> createState() => _CreateRoomDialogState(); +} + +class _CreateRoomDialogState extends ConsumerState<_CreateRoomDialog> { + final _name = TextEditingController(); + final _topic = TextEditingController(); + bool _public = false; + bool _encrypted = true; + + @override + void dispose() { + _name.dispose(); + _topic.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final client = ref.watch(clientProvider); + return _FormDialog( + title: 'New room', + action: 'Create', + fields: [ + TextField( + controller: _name, + autofocus: true, + textCapitalization: TextCapitalization.sentences, + decoration: const InputDecoration(labelText: 'Name'), + ), + const SizedBox(height: 12), + TextField( + controller: _topic, + textCapitalization: TextCapitalization.sentences, + decoration: const InputDecoration(labelText: 'Topic (optional)'), + ), + const SizedBox(height: 8), + StatefulBuilder( + builder: (context, setLocal) => Column( + children: [ + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Public'), + subtitle: const Text('Anyone can find and join it'), + value: _public, + onChanged: (v) => setLocal(() => _public = v), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('End-to-end encrypted'), + subtitle: const Text("Can't be turned off later"), + value: _encrypted, + onChanged: client.encryptionEnabled + ? (v) => setLocal(() => _encrypted = v) + : null, + ), + ], + ), + ), + ], + submit: () async { + final name = _name.text.trim(); + if (name.isEmpty) throw const FormatException('Give the room a name.'); + final topic = _topic.text.trim(); + final roomId = await client.createGroupChat( + groupName: name, + enableEncryption: _encrypted && client.encryptionEnabled, + preset: _public + ? CreateRoomPreset.publicChat + : CreateRoomPreset.privateChat, + visibility: _public ? Visibility.public : Visibility.private, + initialState: [ + if (topic.isNotEmpty) + StateEvent(type: EventTypes.RoomTopic, content: {'topic': topic}), + ], + ); + return roomId; + }, + ); + } +} + +class _JoinRoomDialog extends ConsumerStatefulWidget { + const _JoinRoomDialog(); + + @override + ConsumerState<_JoinRoomDialog> createState() => _JoinRoomDialogState(); +} + +class _JoinRoomDialogState extends ConsumerState<_JoinRoomDialog> { + final _address = TextEditingController(); + + @override + void dispose() { + _address.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final client = ref.watch(clientProvider); + return _FormDialog( + title: 'Join a room', + action: 'Join', + fields: [ + TextField( + controller: _address, + autofocus: true, + autocorrect: false, + decoration: InputDecoration( + labelText: 'Room address', + hintText: '#room:${client.userID?.domain ?? 'example.org'}', + ), + ), + ], + submit: () async { + var address = _address.text.trim(); + if (address.isEmpty) { + throw const FormatException('Enter a room address.'); + } + if (!address.startsWith('#') && !address.startsWith('!')) { + address = '#$address'; + } + if (!address.contains(':')) { + address = '$address:${client.userID!.domain}'; + } + final roomId = await client.joinRoom(address); + if (client.getRoomById(roomId)?.membership != Membership.join) { + await client.waitForRoomInSync(roomId, join: true); + } + return roomId; + }, + ); + } +} diff --git a/apps/mobile/lib/widgets/uia.dart b/apps/mobile/lib/widgets/uia.dart new file mode 100644 index 0000000..15c6d35 --- /dev/null +++ b/apps/mobile/lib/widgets/uia.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +/// Answers a user-interactive authentication request: the homeserver asking +/// the user to prove it's them before a sensitive change, such as uploading +/// cross-signing keys or deleting a session. +/// +/// Warp asks for the account password (`m.login.password`); a server that +/// needs nothing (`m.login.dummy`) is answered without asking. Any other +/// stage can't be done in the app and cancels the request. +Future handleUia( + BuildContext context, + Client client, + UiaRequest request, +) async { + if (request.state != UiaRequestState.waitForUser) return; + final stages = request.nextStages; + if (stages.contains(AuthenticationTypes.dummy)) { + await request.completeStage( + AuthenticationData( + type: AuthenticationTypes.dummy, + session: request.session, + ), + ); + return; + } + if (!stages.contains(AuthenticationTypes.password)) { + request.cancel( + Exception( + "This server asks for a sign-in step Warp " + "can't do: ${stages.join(', ')}.", + ), + ); + return; + } + + final password = await askPassword(context); + if (password == null) { + request.cancel(Exception('Canceled')); + return; + } + await request.completeStage( + AuthenticationPassword( + session: request.session, + password: password, + identifier: AuthenticationUserIdentifier(user: client.userID!), + ), + ); +} + +Future askPassword( + BuildContext context, { + String title = 'Confirm your password', + String message = 'To make this change, enter your account password.', +}) { + final controller = TextEditingController(); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + const SizedBox(height: 16), + TextField( + controller: controller, + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + decoration: const InputDecoration(labelText: 'Password'), + onSubmitted: (v) => Navigator.pop(context, v), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, controller.text), + child: const Text('Continue'), + ), + ], + ), + ); +} diff --git a/apps/mobile/lib/widgets/verification_sheet.dart b/apps/mobile/lib/widgets/verification_sheet.dart new file mode 100644 index 0000000..b952452 --- /dev/null +++ b/apps/mobile/lib/widgets/verification_sheet.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:matrix/encryption.dart'; +import 'package:matrix/matrix.dart'; + +import '../theme.dart'; + +/// Runs a verification, incoming or one this session started, to the end: +/// accept, compare emoji, done. Closing the sheet early cancels it. +/// +/// Emoji (or numbers) only for now; scanning QR codes is a later addition. +Future showVerificationSheet( + BuildContext context, + KeyVerification request, +) => showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + builder: (_) => _VerificationSheet(request), +); + +class _VerificationSheet extends StatefulWidget { + const _VerificationSheet(this.request); + final KeyVerification request; + + @override + State<_VerificationSheet> createState() => _VerificationSheetState(); +} + +class _VerificationSheetState extends State<_VerificationSheet> { + KeyVerification get request => widget.request; + bool _busy = false; + + @override + void initState() { + super.initState(); + request.onUpdate = _onUpdate; + _onUpdate(); + } + + @override + void dispose() { + request.onUpdate = null; + if (!request.isDone) request.cancel('m.user'); + super.dispose(); + } + + void _onUpdate() { + // Steps that need no decision from the user. + if (request.state == KeyVerificationState.askChoice && + request.possibleMethods.contains(EventTypes.Sas)) { + request.continueVerification(EventTypes.Sas); + } else if (request.state == KeyVerificationState.askSSSS) { + request.openSSSS(skip: true); + } + if (mounted) setState(() {}); + } + + Future _run(Future Function() action) async { + setState(() => _busy = true); + try { + await action(); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + bool get _isSelf => request.userId == request.client.userID; + + String get _who { + if (_isSelf) return 'your other session'; + final user = request.room?.unsafeGetUserFromMemoryOrFallback( + request.userId, + ); + return user?.calcDisplayname() ?? request.userId; + } + + @override + Widget build(BuildContext context) { + final c = context.warp; + final text = Theme.of(context).textTheme; + final (title, body, actions) = switch (request.state) { + KeyVerificationState.askAccept => ( + _isSelf ? 'Verify this session?' : 'Verification request', + _isSelf + ? 'Another of your sessions wants to verify this one. Accept, ' + 'then compare the emoji on both screens.' + : '$_who wants to verify you. Accept, then compare the emoji ' + 'on both screens.', + [ + OutlinedButton( + onPressed: _busy ? null : () => _run(request.rejectVerification), + child: const Text('Decline'), + ), + FilledButton( + onPressed: _busy ? null : () => _run(request.acceptVerification), + child: const Text('Accept'), + ), + ], + ), + KeyVerificationState.askSas => ( + 'Compare emoji', + 'Check that these appear, in the same order, on $_who\'s screen.', + [ + OutlinedButton( + onPressed: _busy ? null : () => _run(request.rejectSas), + child: const Text("They don't match"), + ), + FilledButton( + onPressed: _busy ? null : () => _run(request.acceptSas), + child: const Text('They match'), + ), + ], + ), + KeyVerificationState.waitingSas => ( + 'Waitingโ€ฆ', + 'Waiting for $_who to confirm the emoji match.', + [], + ), + KeyVerificationState.done => ( + 'Verified', + _isSelf + ? 'This session is verified. It can read your encrypted ' + 'messages, and the people you talk to will trust it.' + : '$_who is verified. Messages from their verified sessions ' + 'are trusted.', + [ + FilledButton( + onPressed: () => Navigator.pop(context), + child: const Text('Done'), + ), + ], + ), + KeyVerificationState.error => ( + 'Verification stopped', + _cancelText(request.canceledCode, request.canceledReason), + [ + FilledButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + _ => ('Verifyingโ€ฆ', 'Waiting for $_who to accept.', []), + }; + + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(title, style: text.titleLarge), + const SizedBox(height: 8), + Text(body, style: text.bodyMedium?.copyWith(color: c.inkMuted)), + if (request.state == KeyVerificationState.askSas) ...[ + const SizedBox(height: 24), + _SasView(request), + ], + if (actions.isEmpty && + request.state != KeyVerificationState.done) ...[ + const SizedBox(height: 24), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + if (actions.isNotEmpty) ...[ + const SizedBox(height: 24), + Row( + children: [ + for (final (i, a) in actions.indexed) ...[ + if (i > 0) const SizedBox(width: 12), + Expanded(child: a), + ], + ], + ), + ], + ], + ), + ), + ); + } + + static String _cancelText(String? code, String? reason) => switch (code) { + 'm.user' => 'The verification was canceled.', + 'm.timeout' => 'The verification took too long.', + 'm.mismatched_sas' || + 'm.key_mismatch' => "The emoji didn't match. Nobody was verified.", + 'm.accepted' => 'Another of your sessions answered the request.', + _ => reason ?? 'Something went wrong.', + }; +} + +class _SasView extends StatelessWidget { + const _SasView(this.request); + final KeyVerification request; + + @override + Widget build(BuildContext context) { + final text = Theme.of(context).textTheme; + if (!request.sasTypes.contains('emoji')) { + return Text( + request.sasNumbers.join(' '), + textAlign: TextAlign.center, + style: text.headlineMedium?.copyWith(fontFamily: fontMono), + ); + } + return Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 16, + children: [ + for (final emoji in request.sasEmojis) + SizedBox( + width: 72, + child: Column( + children: [ + Text(emoji.emoji, style: const TextStyle(fontSize: 36)), + const SizedBox(height: 4), + Text( + emoji.name, + textAlign: TextAlign.center, + style: text.labelMedium, + ), + ], + ), + ), + ], + ); + } +} diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock new file mode 100644 index 0000000..922d487 --- /dev/null +++ b/apps/mobile/pubspec.lock @@ -0,0 +1,1250 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + android_file_picker: + dependency: transitive + description: + name: android_file_picker + sha256: e94229cc41c4bb5fb6a2254146340a8b650664c99b5c93b9001f66c3375d0060 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + archive: + dependency: transitive + description: + name: archive + sha256: "0d58d1fa1fddf6588251ccf2819f3fb3b6d0452c1b2813ac7413a05592bfbefe" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + blurhash_dart: + dependency: transitive + description: + name: blurhash_dart + sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + canonical_json: + dependency: transitive + description: + name: canonical_json + sha256: "618db0b1ea4238a3ef2f325908beee6269550d73b69dc5220456b75ce3cec072" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + change_case: + dependency: transitive + description: + name: change_case + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 + url: "https://pub.dev" + source: hosted + version: "0.3.5+5" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + dbus: + dependency: transitive + description: + name: dbus + sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 + url: "https://pub.dev" + source: hosted + version: "0.7.15" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "98c0b156b6380ba55bc767a14e2e6b3feb246e713ad40092424596fa79005bea" + url: "https://pub.dev" + source: hosted + version: "13.1.0" + file_picker_darwin: + dependency: transitive + description: + name: file_picker_darwin + sha256: a5a48960703e7faa880544ef188491f442a93959b6afb5cae57575e5dc836589 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + file_picker_linux: + dependency: transitive + description: + name: file_picker_linux + sha256: e7db600f50672ce5ebbe422ac0906e0f3d2a476188cc93e7126afee12bf08063 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + file_picker_platform_interface: + dependency: transitive + description: + name: file_picker_platform_interface + sha256: "336e7168306834a2b68e6168ed768e00d3fabeac23154d5793e277ca9f97cf20" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + file_picker_web: + dependency: transitive + description: + name: file_picker_web + sha256: a2288008280aabe371aff029466b3405e60838e0a1d714767488c59b926c8b8c + url: "https://pub.dev" + source: hosted + version: "4.2.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab + url: "https://pub.dev" + source: hosted + version: "0.9.4+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4 + url: "https://pub.dev" + source: hosted + version: "0.9.5+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec + url: "https://pub.dev" + source: hosted + version: "0.9.3+6" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "9ee28742343dd13df758bb4185e8ded37ac2d1aeca6648c3ba7862c99797517e" + url: "https://pub.dev" + source: hosted + version: "22.3.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "3b89a4bf095fa5f8a99beeabf982c5bd506c3ad25e7b9e61da945e994dc5b615" + url: "https://pub.dev" + source: hosted + version: "8.0.2" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd" + url: "https://pub.dev" + source: hosted + version: "12.2.0" + flutter_local_notifications_web: + dependency: transitive + description: + name: flutter_local_notifications_web + sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3" + url: "https://pub.dev" + source: hosted + version: "3.4.3" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: ec69331e2335ab4d456c0a9fc700892c2c85b72b9c0cb39df3ecfe8833d5d5c8 + url: "https://pub.dev" + source: hosted + version: "2.13.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: d4e1fb6b2cb524868929e78dc0282fa000554b22060fb53789dc481c9fc95bb8 + url: "https://pub.dev" + source: hosted + version: "11.2.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: a031ceac9b070e62ef183cd7f7d1a8798ebde402d5049f0fe6eb3a51bd797a21 + url: "https://pub.dev" + source: hosted + version: "0.4.3" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: caa75bd78f017422912e3a904933113b2428eb79f981ac37acd2dd2a700458ea + url: "https://pub.dev" + source: hosted + version: "3.0.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1" + url: "https://pub.dev" + source: hosted + version: "4.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_vodozemac: + dependency: "direct main" + description: + name: flutter_vodozemac + sha256: "209e7661da113a5f889ae55d574c48f245145def64df9a9fcac04d147e6f9d4e" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + html: + dependency: transitive + description: + name: html + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" + url: "https://pub.dev" + source: hosted + version: "0.15.7" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: e71b1bd9e40124041128d60fc3034c7b1598e1bba4e570bba0c70301c817013a + url: "https://pub.dev" + source: hosted + version: "1.2.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8a7823d5d3e884f7780829d85d92110d072bdcd9ed48e2be158d2abe91a912d4" + url: "https://pub.dev" + source: hosted + version: "0.8.13+25" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4fb88d4350ffc208c2a09fc321db1f80282688716b8cfe10c8c80acc5ae8934a" + url: "https://pub.dev" + source: hosted + version: "0.8.13+10" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "2dee4b4e3be429db71472c9c559c981498070a61af1ba82e0e4986fc5dadc042" + url: "https://pub.dev" + source: hosted + version: "2.11.2" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + jni: + dependency: transitive + description: + name: jni + sha256: "6b9ca0602fef0230e83149afbe3f9f8227e94f2bbe84a1da4f14d699742f402d" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "0dd33d67797560f66e961a6053882d4df54bd345bf063b8fe7ea133f7ad8b191" + url: "https://pub.dev" + source: hosted + version: "1.0.4+1" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 + url: "https://pub.dev" + source: hosted + version: "7.3.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + matrix: + dependency: "direct main" + description: + name: matrix + sha256: "5c7afb10467790769c87285545eb2de79c40696dcc1f4fe515b63c69674671da" + url: "https://pub.dev" + source: hosted + version: "14.1.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mime: + dependency: "direct main" + description: + name: mime + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + native_toolchain_cmake: + dependency: transitive + description: + name: native_toolchain_cmake + sha256: de90e8952bad0684bdcff4d674c53d961f5cd1fdcf3a0255a669a35986c4ac1d + url: "https://pub.dev" + source: hosted + version: "0.2.7" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec + url: "https://pub.dev" + source: hosted + version: "3.2.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e + url: "https://pub.dev" + source: hosted + version: "6.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + random_string: + dependency: transitive + description: + name: random_string + sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a" + url: "https://pub.dev" + source: hosted + version: "3.4.3" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a + url: "https://pub.dev" + source: hosted + version: "0.2.2" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + sdp_transform: + dependency: transitive + description: + name: sdp_transform + sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + slugify: + dependency: transitive + description: + name: slugify + sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: "273218a1678360a3127fcf15e543381d9b813d413d783bc6e9a53eed4d2eb8fe" + url: "https://pub.dev" + source: hosted + version: "2.4.4+2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: e4f659c14bdd14464127fcd234a7a96f61fe0f539b8568e5a38f99f5a568a835 + url: "https://pub.dev" + source: hosted + version: "2.4.4+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: b80e94ff0d551d5f1f8c7d14348d29b576cca3ae30f6e195a7c380552c32b1e0 + url: "https://pub.dev" + source: hosted + version: "2.5.14" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "8de68fc306040f992aaf378750c16f95e4298c4bb150dd45091d75e42b1f08ee" + url: "https://pub.dev" + source: hosted + version: "2.4.4+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: c9177b2f3150ed0647b90a94307c28452fe0a5401863044216aed83addd6c7c5 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "4c7fe79840389aaeaf05fd093f795b631b5a98e2bd28d54e555c100f4a9c7a1c" + url: "https://pub.dev" + source: hosted + version: "3.5.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: b3d516dbd3757ff2a458735e4f6cbac9ff261f576f6e87bcb17feabb39e657c5 + url: "https://pub.dev" + source: hosted + version: "3.5.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: "9f4e5a1a0e4fef7dd3424842c0575db01bfb7a11730e8d55753fcd5c5c98767b" + url: "https://pub.dev" + source: hosted + version: "0.11.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unifiedpush: + dependency: "direct main" + description: + name: unifiedpush + sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + unifiedpush_android: + dependency: transitive + description: + name: unifiedpush_android + sha256: a902e505e91c8e28b54b379bb27f255f66df409274a97af24d6719f12f0d13dc + url: "https://pub.dev" + source: hosted + version: "3.5.0" + unifiedpush_linux: + dependency: transitive + description: + name: unifiedpush_linux + sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + unifiedpush_platform_interface: + dependency: transitive + description: + name: unifiedpush_platform_interface + sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + unifiedpush_storage_interface: + dependency: transitive + description: + name: unifiedpush_storage_interface + sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: b4f4d2aff27d65a5fc37f400d4c09e55d7535359315acbe3b895a9add4de3a23 + url: "https://pub.dev" + source: hosted + version: "0.3.3" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" + url: "https://pub.dev" + source: hosted + version: "6.3.33" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" + url: "https://pub.dev" + source: hosted + version: "6.4.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" + url: "https://pub.dev" + source: hosted + version: "3.2.6" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "89b11d62e1040ee681af3b4f1c97a803ecbf010c6e8b9c5896ee5b08c3bf7b78" + url: "https://pub.dev" + source: hosted + version: "15.4.0" + vodozemac: + dependency: transitive + description: + name: vodozemac + sha256: efce261575f7de97ebd052b16775cd57f940e8212af47d288050c166b9f9a167 + url: "https://pub.dev" + source: hosted + version: "0.8.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webcrypto: + dependency: transitive + description: + name: webcrypto + sha256: "9885ed99a846191542dd6ca820c83a098117df56be57453832233a3fef68f3f2" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + webpush_encryption: + dependency: transitive + description: + name: webpush_encryption + sha256: "5b83272b91acda6ae515fcd980c94f06bf413702282497c5a68f5dfc64fed27f" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f + url: "https://pub.dev" + source: hosted + version: "1.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + url: "https://pub.dev" + source: hosted + version: "6.4.0" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "05c231fd7b23d2380f14c5cc10b7b93d60d4fa4a2fb4e0f032de27e44b5560e9" + url: "https://pub.dev" + source: hosted + version: "0.5.2" + windows_file_picker: + dependency: transitive + description: + name: windows_file_picker + sha256: "18452aa293de3b6961c6d05b0e71b4be7bb90c67635abdd3aa5c4e75dd875072" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" +sdks: + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.44.0" diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml new file mode 100644 index 0000000..fa3517d --- /dev/null +++ b/apps/mobile/pubspec.yaml @@ -0,0 +1,93 @@ +name: warp +description: "Warp: a Matrix client" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.12.2 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + matrix: ^14.1.0 + flutter_vodozemac: ^0.8.1 + flutter_riverpod: ^3.4.3 + flutter_secure_storage: ^11.2.0 + sqflite: ^2.4.4+2 + path_provider: ^2.1.6 + path: ^1.9.1 + file_picker: ^13.1.0 + image_picker: ^1.2.4 + flutter_local_notifications: ^22.3.2 + unifiedpush: ^6.2.0 + http: ^1.6.0 + intl: ^0.20.3 + url_launcher: ^6.3.3 + mime: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.14.4 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + uses-material-design: true + assets: + - assets/logo/ + # The brand fonts (SIL Open Font License), converted from the woff2 files + # in apps/ui/src/styles/fonts: Flutter can't load woff2. + fonts: + - family: Outfit + fonts: + - asset: assets/fonts/Outfit-Variable.ttf + - family: Figtree + fonts: + - asset: assets/fonts/Figtree-Variable.ttf + - family: JetBrains Mono + fonts: + - asset: assets/fonts/JetBrainsMono-Variable.ttf + +# `dart run flutter_launcher_icons` regenerates the Android and iOS icons. +flutter_launcher_icons: + android: true + ios: true + image_path: tool/icons/app-icon.png + adaptive_icon_background: "#00A389" + adaptive_icon_foreground: tool/icons/foreground.png + adaptive_icon_foreground_inset: 28 + remove_alpha_ios: true diff --git a/apps/mobile/test/format_test.dart b/apps/mobile/test/format_test.dart new file mode 100644 index 0000000..66cf29d --- /dev/null +++ b/apps/mobile/test/format_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:warp/matrix/client.dart'; +import 'package:warp/util/format.dart'; + +void main() { + group('formatBytes', () { + test('bytes', () => expect(formatBytes(980), '980 B')); + test('kilobytes', () => expect(formatBytes(12345), '12 KB')); + test('small megabytes keep a decimal', () { + expect(formatBytes(3400000), '3.4 MB'); + }); + test('gigabytes', () => expect(formatBytes(2000000000), '2.0 GB')); + }); + + group('formatListTime', () { + final now = DateTime(2026, 10, 18, 15, 30); + test('today is the time', () { + expect(formatListTime(DateTime(2026, 10, 18, 9, 5), now: now), '09:05'); + }); + test('yesterday', () { + expect(formatListTime(DateTime(2026, 10, 17, 23), now: now), 'Yesterday'); + }); + test('this week is the weekday', () { + expect(formatListTime(DateTime(2026, 10, 14), now: now), 'Wed'); + }); + }); + + group('formatDay', () { + final now = DateTime(2026, 10, 18, 15, 30); + test( + 'today', + () => expect(formatDay(DateTime(2026, 10, 18), now: now), 'Today'), + ); + test('yesterday', () { + expect(formatDay(DateTime(2026, 10, 17), now: now), 'Yesterday'); + }); + }); + + group('serverOfUserId', () { + test('a full user ID', () { + expect(serverOfUserId('@alice:example.org'), 'example.org'); + }); + test('with a port', () { + expect(serverOfUserId(' @bob:localhost:8008 '), 'localhost:8008'); + }); + test('a bare username', () => expect(serverOfUserId('alice'), isNull)); + test('no server', () => expect(serverOfUserId('@alice:'), isNull)); + }); +} diff --git a/apps/mobile/test/timeline_test.dart b/apps/mobile/test/timeline_test.dart new file mode 100644 index 0000000..4c908dc --- /dev/null +++ b/apps/mobile/test/timeline_test.dart @@ -0,0 +1,95 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:warp/matrix/timeline.dart'; + +Entry msg(String sender, DateTime time) => + (sender: sender, time: time, kind: EntryKind.message); +Entry notice(DateTime time) => + (sender: '@admin:x', time: time, kind: EntryKind.notice); +Entry hidden(DateTime time) => + (sender: '@a:x', time: time, kind: EntryKind.hidden); + +/// Describes items compactly: `D` for a day, `m`, +/// `n` for a notice. +List describe(List items) => [ + for (final item in items) + switch (item) { + DayItem() => 'D', + EventItem(:final index, :final kind) when kind == EntryKind.notice => + 'n$index', + EventItem(:final index, :final firstInGroup, :final lastInGroup) => + 'm$index${firstInGroup ? 'F' : ''}${lastInGroup ? 'L' : ''}', + }, +]; + +void main() { + final t0 = DateTime(2026, 10, 18, 12); + + test('empty', () { + expect(projectTimeline([]), isEmpty); + }); + + test('a run from one sender is one group', () { + // Newest first, as the SDK keeps them. + final items = projectTimeline([ + msg('@a:x', t0.add(const Duration(minutes: 2))), + msg('@a:x', t0.add(const Duration(minutes: 1))), + msg('@a:x', t0), + ]); + expect(describe(items), ['m0L', 'm1', 'm2F', 'D']); + }); + + test('a new sender starts a new group', () { + final items = projectTimeline([ + msg('@b:x', t0.add(const Duration(minutes: 1))), + msg('@a:x', t0), + ]); + expect(describe(items), ['m0FL', 'm1FL', 'D']); + }); + + test('a long pause starts a new group', () { + final items = projectTimeline([ + msg('@a:x', t0.add(const Duration(minutes: 6))), + msg('@a:x', t0), + ]); + expect(describe(items), ['m0FL', 'm1FL', 'D']); + }); + + test('hidden events are dropped and do not break a group', () { + final items = projectTimeline([ + msg('@a:x', t0.add(const Duration(minutes: 2))), + hidden(t0.add(const Duration(minutes: 1))), + msg('@a:x', t0), + ]); + expect(describe(items), ['m0L', 'm2F', 'D']); + }); + + test('a notice breaks a group', () { + final items = projectTimeline([ + msg('@a:x', t0.add(const Duration(minutes: 2))), + notice(t0.add(const Duration(minutes: 1))), + msg('@a:x', t0), + ]); + expect(describe(items), ['m0FL', 'n1', 'm2FL', 'D']); + }); + + test('a day separator above the first event of each day', () { + final yesterday = t0.subtract(const Duration(days: 1)); + final items = projectTimeline([ + msg('@a:x', t0), + msg('@a:x', yesterday.add(const Duration(minutes: 1))), + msg('@a:x', yesterday), + ]); + expect(describe(items), ['m0FL', 'D', 'm1L', 'm2F', 'D']); + expect((items[1] as DayItem).day, DateTime(2026, 10, 18)); + expect((items[4] as DayItem).day, DateTime(2026, 10, 17)); + }); + + test('messages either side of midnight are separate groups', () { + final midnight = DateTime(2026, 10, 18); + final items = projectTimeline([ + msg('@a:x', midnight.add(const Duration(minutes: 1))), + msg('@a:x', midnight.subtract(const Duration(minutes: 1))), + ]); + expect(describe(items), ['m0FL', 'D', 'm1FL', 'D']); + }); +} diff --git a/apps/mobile/tool/icons/app-icon.png b/apps/mobile/tool/icons/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..682ee6c93885c5a5c5e81ed707e7a4424a8a6c70 GIT binary patch literal 34721 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 literal 0 HcmV?d00001 diff --git a/apps/mobile/tool/icons/foreground.png b/apps/mobile/tool/icons/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf23174e7979dd9218283ee8f1071255ba67438 GIT binary patch literal 23940 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 IuHef41|(!3zyJUM literal 0 HcmV?d00001