diff --git a/.github/workflows/gcp_backend.yml b/.github/workflows/gcp_backend.yml
index 9474522da..c9cb728b2 100644
--- a/.github/workflows/gcp_backend.yml
+++ b/.github/workflows/gcp_backend.yml
@@ -84,13 +84,16 @@ jobs:
region: ${{ env.REGION }}
image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}
- - name: Deploy ${{ env.SERVICE }}-listen to Cloud Run
- id: deploy-backend-listen
- uses: google-github-actions/deploy-cloudrun@v2
- with:
- service: ${{ env.SERVICE }}-listen
- region: ${{ env.REGION }}
- image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}
+ - name: Connect to GKE cluster
+ run: |
+ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
+ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
+ sudo apt-get update && sudo apt-get install google-cloud-cli-gke-gcloud-auth-plugin -y
+ gcloud container clusters get-credentials ${{ vars.GKE_CLUSTER }} --region ${{ env.REGION }} --project ${{ vars.GCP_PROJECT_ID }}
+
+ - name: Deploy ${{ env.SERVICE }}-listen to GKE
+ run: |
+ kubectl -n ${{ vars.ENV }}-omi-backend rollout restart deploy ${{ vars.ENV }}-omi-backend-listen
- name: Deploy ${{ env.SERVICE }}-integration to Cloud Run
id: deploy-backend-integration
diff --git a/.gitignore b/.gitignore
index a9a7798aa..632e349e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,10 +25,9 @@ build/
dist/
.build/
.swiftpm/
-# VS Code - exclude everything except specific files
+
+# VS Code
.vscode/*
-!.vscode/launch.json
-!.vscode/extensions.json
# Android specific
**/android/**/build/
@@ -186,3 +185,4 @@ omiGlass/.expo
# Logs
/backend/logs/
+app/.fvm/
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 7f40c923e..000000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Uvicorn (backend)",
- "type": "python",
- "request": "launch",
- "module": "uvicorn",
- "cwd": "${workspaceFolder}/backend",
- "args": [
- "main:app",
- "--reload",
- "--env-file", ".dev.env",
- // "--host", "0.0.0.0",
- "--port", "8000",
- // "--log-level", "debug",
- // "--log-config", "logging_config.json"
- ],
- "jinja": true,
- "python": "${workspaceFolder}/backend/venv/bin/python",
- "justMyCode": false,
- "env": {
- "PYTHONPATH": "${workspaceFolder}/backend",
- "LD_LIBRARY_PATH": "/usr/local/lib:/opt/homebrew/lib:${env:LD_LIBRARY_PATH}",
- "DYLD_LIBRARY_PATH": "/opt/homebrew/lib:${env:DYLD_LIBRARY_PATH}",
- "LIBRARY_PATH": "/opt/homebrew/lib:${env:LIBRARY_PATH}",
- "PKG_CONFIG_PATH": "/opt/homebrew/lib/pkgconfig:${env:PKG_CONFIG_PATH}"
- }
- },
- {
- "name": "Flutter (dev)",
- "type": "dart",
- "request": "launch",
- "program": "${workspaceFolder}/app/lib/main.dart",
- "cwd": "${workspaceFolder}/app",
- "flutterMode": "debug",
- "args": [
- "--flavor",
- "dev"
- ]
- },
- {
- "name": "Flutter (dev) - iOS Specific Device",
- "type": "dart",
- "request": "launch",
- "program": "${workspaceFolder}/app/lib/main.dart",
- "cwd": "${workspaceFolder}/app",
- "flutterMode": "debug",
- "args": [
- "--flavor",
- "dev",
- "-d",
- "00008130-000C5196386B8D3A"
- ]
- },
- {
- "name": "Flutter Run (Terminal)",
- "type": "node",
- "request": "launch",
- "runtimeExecutable": "flutter",
- "cwd": "${workspaceFolder}/app",
- "args": [
- "run",
- "--flavor",
- "dev",
- "-d",
- "00008130-000C5196386B8D3A"
- ],
- "console": "integratedTerminal"
- }
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index c576fdb62..e13322302 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ transcriptions of meetings, chats, and voice memos wherever you are.
-[Homepage](https://omi.me/) | [Documentation](https://docs.omi.me/) | [Buy Consumer device](https://www.omi.me/cart/50230946562340:1) | [Buy Developer Kit](https://www.omi.me/products/omi-dev-kit-2)
+[Homepage](https://omi.me/) | [Documentation](https://docs.omi.me/) | [Buy omi Developer Kit](https://www.omi.me/products/omi-dev-kit-2) | [Buy Omi Glass Dev Kit](https://www.omi.me/glass)
diff --git a/app/.fvmrc b/app/.fvmrc
new file mode 100644
index 000000000..e1504264f
--- /dev/null
+++ b/app/.fvmrc
@@ -0,0 +1,3 @@
+{
+ "flutter": "3.24.1"
+}
\ No newline at end of file
diff --git a/app/README.md b/app/README.md
index 40cd56f88..ac0858933 100644
--- a/app/README.md
+++ b/app/README.md
@@ -6,6 +6,8 @@ The Omi App is a Flutter-based mobile application that serves as the companion a
### Quick Setup
+Before getting started, make sure your device is connected and unlocked. If you're using an iPhone, ensure that Developer Mode is enabled — you can toggle this in the iPhone settings. For Android devices, make sure the device is connected and USB debugging is enabled in Developer Options
+
1. Navigate to the app directory:
```bash
cd app
@@ -19,12 +21,18 @@ The Omi App is a Flutter-based mobile application that serves as the companion a
# For Android
bash setup.sh android
```
+
+3. Ensure GitHub SSH access is set up correctly for pulling certificates from repositories. After running the command below, if you're prompted for a passphrase, enter your SSH passphrase — or simply press Enter/Return if you haven't set one.
+ ```bash
+ cd ~/.ssh; ssh-add
+ ```
-3. Run the app:
+4. To run the app, navigate to the app directory and use the following command:
```bash
flutter run --flavor dev
```
+
### Building and Deploying to iPhone
To build and deploy the app to an iPhone so it can run independently from your laptop:
diff --git a/app/android/settings.gradle b/app/android/settings.gradle
index 591e609fb..63dbc5016 100644
--- a/app/android/settings.gradle
+++ b/app/android/settings.gradle
@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.1.4" apply false
+ id "com.android.application" version "8.6.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj
index 9cf59c0a9..8141bb493 100644
--- a/app/ios/Runner.xcodeproj/project.pbxproj
+++ b/app/ios/Runner.xcodeproj/project.pbxproj
@@ -15,9 +15,9 @@
29B635C199B3BDE0F2BE8EC5 /* devRelease.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 79B00761CB247C7EC7C8983B /* devRelease.xcconfig */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
426B89AD2C46919D00E24442 /* Config in Resources */ = {isa = PBXBuildFile; fileRef = 426B89AC2C46919D00E24442 /* Config */; };
- 63AF53B7F1AB365736C79AB8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C69A653D709374662CCB075E /* Pods_Runner.framework */; };
6436409A27A31CD800820AF7 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6436409C27A31CD800820AF7 /* InfoPlist.strings */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 8ECFBE4905FB83E862F3F961 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA3E73408D04A71ABEA5167 /* Pods_Runner.framework */; };
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 */; };
@@ -52,14 +52,16 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 072872AC24251C3536B9BABF /* Pods-Runner.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig"; sourceTree = ""; };
08E3947A4217EF205E22BD44 /* prodDebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = prodDebug.xcconfig; path = Flutter/prodDebug.xcconfig; sourceTree = ""; };
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 = ""; };
17DD34CA2CFE6F3845E13C99 /* devDebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = devDebug.xcconfig; path = Flutter/devDebug.xcconfig; sourceTree = ""; };
+ 18FFC3626FAEB6C0E2F8B8AC /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; };
35656BCB5CBAA7C0BB162EE0 /* prodRelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = prodRelease.xcconfig; path = Flutter/prodRelease.xcconfig; sourceTree = ""; };
- 3724F354CEEDF26CAA89DE63 /* Pods-Runner.release-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-prod.xcconfig"; sourceTree = ""; };
+ 3A57DC804B7F6C11D18054D4 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-dev.xcconfig"; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 3B84245E7DA0251DE76CA384 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 3BA3E73408D04A71ABEA5167 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
42127D962C283C250005AB40 /* OneSignalCore.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:J3J28YJX9L:OneSignal, Inc."; lastKnownFileType = wrapper.xcframework; name = OneSignalCore.xcframework; path = Pods/OneSignalXCFramework/iOS_SDK/OneSignalSDK/OneSignal_Core/OneSignalCore.xcframework; sourceTree = ""; };
42127D9A2C283C310005AB40 /* OneSignalExtension.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:J3J28YJX9L:OneSignal, Inc."; lastKnownFileType = wrapper.xcframework; name = OneSignalExtension.xcframework; path = Pods/OneSignalXCFramework/iOS_SDK/OneSignalSDK/OneSignal_Extension/OneSignalExtension.xcframework; sourceTree = ""; };
42127D9D2C283C400005AB40 /* OneSignalOSCore.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:J3J28YJX9L:OneSignal, Inc."; lastKnownFileType = wrapper.xcframework; name = OneSignalOSCore.xcframework; path = Pods/OneSignalXCFramework/iOS_SDK/OneSignalSDK/OneSignal_OSCore/OneSignalOSCore.xcframework; sourceTree = ""; };
@@ -67,7 +69,6 @@
42476A832C5E369200426330 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
426B89AC2C46919D00E24442 /* Config */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Config; sourceTree = ""; };
556416C8748DD3DD2C06A9DB /* devProfile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = devProfile.xcconfig; path = Flutter/devProfile.xcconfig; sourceTree = ""; };
- 5CC54DB1539C2E32157E773D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
5F7FD4162BF1232500923839 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
5FCEB2FA2C27586C00B17EE8 /* RunnerDebug-prod.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RunnerDebug-prod.entitlements"; sourceTree = ""; };
5FCEB2FB2C27587100B17EE8 /* RunnerProfile-prod.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RunnerProfile-prod.entitlements"; sourceTree = ""; };
@@ -75,12 +76,11 @@
5FCEB2FD2C2758BA00B17EE8 /* RunnerDebug-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RunnerDebug-dev.entitlements"; sourceTree = ""; };
5FCEB2FE2C2758BD00B17EE8 /* RunnerProfile-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RunnerProfile-dev.entitlements"; sourceTree = ""; };
5FCEB2FF2C2758C000B17EE8 /* RunnerRelease-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RunnerRelease-dev.entitlements"; sourceTree = ""; };
+ 6F1A7E959CF9BBEC39BA4471 /* Pods-Runner.debug-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-prod.xcconfig"; 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 = ""; };
- 79A3E5C306D624AE31145607 /* Pods-Runner.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-prod.xcconfig"; sourceTree = ""; };
79B00761CB247C7EC7C8983B /* devRelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = devRelease.xcconfig; path = Flutter/devRelease.xcconfig; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 92800DE2DBE33C9182F3F582 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-dev.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; };
@@ -88,15 +88,15 @@
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 = ""; };
- 9E42219331AC03D460762ACB /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; };
+ 9D6DA65A5DFCF877576F2AEC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9F07595217A5DEEA0729C7FE /* Pods-Runner.release-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-prod.xcconfig"; sourceTree = ""; };
ACE7BEFB9654B6DF5B9E1790 /* prodLaunchScreen.storyboard */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.storyboard; name = prodLaunchScreen.storyboard; path = Runner/prodLaunchScreen.storyboard; sourceTree = ""; };
B52E83A1CC630E163A04DC50 /* prodProfile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = prodProfile.xcconfig; path = Flutter/prodProfile.xcconfig; sourceTree = ""; };
- C69A653D709374662CCB075E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- CAA9769D950FF08AFB9038AC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ B91448D531463AC31409FBDE /* Pods-Runner.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-prod.xcconfig"; sourceTree = ""; };
+ C797F1462626E744BA8F2F4B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
CF0FE6142D61FDC10072C10D /* Custom.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Custom.xcconfig; path = Flutter/Custom.xcconfig; sourceTree = ""; };
- D03DCC70D4791528CB0929E7 /* Pods-Runner.debug-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-prod.xcconfig"; sourceTree = ""; };
- E4D40465AE1C862B18960D63 /* Pods-Runner.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig"; sourceTree = ""; };
E7A9F5F37DDF9FA87AC8A5CD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
+ E8303C81065F720515309566 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
EF770A936DE2AA63387FD198 /* devLaunchScreen.storyboard */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.storyboard; name = devLaunchScreen.storyboard; path = Runner/devLaunchScreen.storyboard; sourceTree = ""; };
/* End PBXFileReference section */
@@ -105,7 +105,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 63AF53B7F1AB365736C79AB8 /* Pods_Runner.framework in Frameworks */,
+ 8ECFBE4905FB83E862F3F961 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -120,7 +120,7 @@
42127D9D2C283C400005AB40 /* OneSignalOSCore.xcframework */,
42127D9A2C283C310005AB40 /* OneSignalExtension.xcframework */,
42127D962C283C250005AB40 /* OneSignalCore.xcframework */,
- C69A653D709374662CCB075E /* Pods_Runner.framework */,
+ 3BA3E73408D04A71ABEA5167 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -128,15 +128,15 @@
68E21E60C4FBF7F520F1D656 /* Pods */ = {
isa = PBXGroup;
children = (
- 3B84245E7DA0251DE76CA384 /* Pods-Runner.debug.xcconfig */,
- 5CC54DB1539C2E32157E773D /* Pods-Runner.release.xcconfig */,
- CAA9769D950FF08AFB9038AC /* Pods-Runner.profile.xcconfig */,
- D03DCC70D4791528CB0929E7 /* Pods-Runner.debug-prod.xcconfig */,
- 79A3E5C306D624AE31145607 /* Pods-Runner.profile-prod.xcconfig */,
- 3724F354CEEDF26CAA89DE63 /* Pods-Runner.release-prod.xcconfig */,
- E4D40465AE1C862B18960D63 /* Pods-Runner.debug-dev.xcconfig */,
- 92800DE2DBE33C9182F3F582 /* Pods-Runner.profile-dev.xcconfig */,
- 9E42219331AC03D460762ACB /* Pods-Runner.release-dev.xcconfig */,
+ E8303C81065F720515309566 /* Pods-Runner.debug.xcconfig */,
+ 9D6DA65A5DFCF877576F2AEC /* Pods-Runner.release.xcconfig */,
+ C797F1462626E744BA8F2F4B /* Pods-Runner.profile.xcconfig */,
+ 6F1A7E959CF9BBEC39BA4471 /* Pods-Runner.debug-prod.xcconfig */,
+ B91448D531463AC31409FBDE /* Pods-Runner.profile-prod.xcconfig */,
+ 9F07595217A5DEEA0729C7FE /* Pods-Runner.release-prod.xcconfig */,
+ 072872AC24251C3536B9BABF /* Pods-Runner.debug-dev.xcconfig */,
+ 3A57DC804B7F6C11D18054D4 /* Pods-Runner.profile-dev.xcconfig */,
+ 18FFC3626FAEB6C0E2F8B8AC /* Pods-Runner.release-dev.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -212,7 +212,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
- F9F8C4175EB80A90DC64548E /* [CP] Check Pods Manifest.lock */,
+ 850DCFF539DDE60719A1CB47 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
426B89AB2C46885100E24442 /* Copy Google Plist file */,
97C146EA1CF9000F007C117D /* Sources */,
@@ -221,8 +221,8 @@
42127D8A2C2839B60005AB40 /* Embed Foundation Extensions */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 2F67A041E3B0519B462D484D /* [CP] Embed Pods Frameworks */,
- 1DD18D9D2FBAA8A200EFCE7E /* [CP] Copy Pods Resources */,
+ 1696D7B5D702A72B207AC179 /* [CP] Embed Pods Frameworks */,
+ C78E0E14E2A1712A91339475 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -294,24 +294,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 1DD18D9D2FBAA8A200EFCE7E /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
- 2F67A041E3B0519B462D484D /* [CP] Embed Pods Frameworks */ = {
+ 1696D7B5D702A72B207AC179 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -362,6 +345,28 @@
shellPath = /bin/sh;
shellScript = "# Type a script or drag a sc# Type a script or drag a script file from your workspace to insert its path.\nDEV_GOOGLE_PLIST=\"${PROJECT_DIR}/Config/Dev\"\nPROD_GOOGLE_PLIST=\"${PROJECT_DIR}/Config/Prod\"\n\necho \"build config is ${CONFIGURATION}\"\n\ncase \"${CONFIGURATION}\" in\n\n \"Debug-dev\" | \"AdHoc_Staging\" | \"Release-dev\" | \"Debug\")\n cp -r \"$DEV_GOOGLE_PLIST/GoogleService-Info.plist\" \"${PROJECT_DIR}/Runner/GoogleService-Info.plist\" ;;\n \"Debug-prod\" | \"Profile\" |\"Release-prod\" |\"Release\")\n cp -r \"$PROD_GOOGLE_PLIST/GoogleService-Info.plist\" \"${PROJECT_DIR}/Runner/GoogleService-Info.plist\" ;;\n\n *)\n ;;\nesac\n";
};
+ 850DCFF539DDE60719A1CB47 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -377,26 +382,21 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
- F9F8C4175EB80A90DC64548E /* [CP] Check Pods Manifest.lock */ = {
+ C78E0E14E2A1712A91339475 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ name = "[CP] Copy Pods Resources";
outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -442,7 +442,7 @@
/* Begin XCBuildConfiguration section */
0CE6142D2C21F36000559F1E /* Debug-prod */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = D03DCC70D4791528CB0929E7 /* Pods-Runner.debug-prod.xcconfig */;
+ baseConfigurationReference = 6F1A7E959CF9BBEC39BA4471 /* Pods-Runner.debug-prod.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -459,7 +459,7 @@
};
0CE6142E2C21F36000559F1E /* Profile-prod */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 79A3E5C306D624AE31145607 /* Pods-Runner.profile-prod.xcconfig */;
+ baseConfigurationReference = B91448D531463AC31409FBDE /* Pods-Runner.profile-prod.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -476,7 +476,7 @@
};
0CE6142F2C21F36000559F1E /* Release-prod */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 3724F354CEEDF26CAA89DE63 /* Pods-Runner.release-prod.xcconfig */;
+ baseConfigurationReference = 9F07595217A5DEEA0729C7FE /* Pods-Runner.release-prod.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -493,7 +493,7 @@
};
0CE614302C21F36000559F1E /* Debug-dev */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = E4D40465AE1C862B18960D63 /* Pods-Runner.debug-dev.xcconfig */;
+ baseConfigurationReference = 072872AC24251C3536B9BABF /* Pods-Runner.debug-dev.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -510,7 +510,7 @@
};
0CE614312C21F36000559F1E /* Profile-dev */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 92800DE2DBE33C9182F3F582 /* Pods-Runner.profile-dev.xcconfig */;
+ baseConfigurationReference = 3A57DC804B7F6C11D18054D4 /* Pods-Runner.profile-dev.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -527,7 +527,7 @@
};
0CE614322C21F36000559F1E /* Release-dev */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 9E42219331AC03D460762ACB /* Pods-Runner.release-dev.xcconfig */;
+ baseConfigurationReference = 18FFC3626FAEB6C0E2F8B8AC /* Pods-Runner.release-dev.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
@@ -546,7 +546,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
@@ -644,7 +644,7 @@
baseConfigurationReference = 08E3947A4217EF205E22BD44 /* prodDebug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -719,7 +719,7 @@
baseConfigurationReference = 35656BCB5CBAA7C0BB162EE0 /* prodRelease.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -789,7 +789,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
@@ -846,7 +846,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
@@ -991,7 +991,7 @@
baseConfigurationReference = 79B00761CB247C7EC7C8983B /* devRelease.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -1062,7 +1062,7 @@
baseConfigurationReference = 556416C8748DD3DD2C06A9DB /* devProfile.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -1132,7 +1132,7 @@
baseConfigurationReference = B52E83A1CC630E163A04DC50 /* prodProfile.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -1202,7 +1202,7 @@
baseConfigurationReference = 17DD34CA2CFE6F3845E13C99 /* devDebug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ARCHS = $ARCHS_STANDARD_64_BIT;
+ ARCHS = "$ARCHS_STANDARD_64_BIT";
ASSETCATALOG_COMPILER_APPICON_NAME = "$(ASSET_PREFIX)AppIcon";
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ANALYZER_NONNULL = YES;
diff --git a/app/lib/backend/auth.dart b/app/lib/backend/auth.dart
index 165a360fa..efcb59d98 100644
--- a/app/lib/backend/auth.dart
+++ b/app/lib/backend/auth.dart
@@ -55,6 +55,7 @@ Future signInWithApple() async {
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
+ accessToken: appleCredential.authorizationCode
);
debugPrint('OAuth Credential created.');
diff --git a/app/lib/backend/http/api/conversations.dart b/app/lib/backend/http/api/conversations.dart
index 252dbfada..93bd550ab 100644
--- a/app/lib/backend/http/api/conversations.dart
+++ b/app/lib/backend/http/api/conversations.dart
@@ -283,6 +283,23 @@ Future setConversationActionItemState(
return response.statusCode == 200;
}
+Future updateActionItemDescription(
+ String conversationId, String oldDescription, String newDescription, int idx) async {
+ var body = {
+ 'old_description': oldDescription,
+ 'description': newDescription,
+ };
+ var response = await makeApiCall(
+ url: '${Env.apiBaseUrl}v1/conversations/$conversationId/action-items/$idx',
+ headers: {},
+ method: 'PATCH',
+ body: jsonEncode(body),
+ );
+ if (response == null) return false;
+ debugPrint('updateActionItemDescription: ${response.body}');
+ return response.statusCode == 200;
+}
+
Future deleteConversationActionItem(String conversationId, ActionItem item) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/$conversationId/action-items',
@@ -381,7 +398,6 @@ Future<(List, int, int)> searchConversationsServer(
return ([], 0, 0);
}
-
Future testConversationPrompt(String prompt, String conversationId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/$conversationId/test-prompt',
@@ -392,9 +408,9 @@ Future testConversationPrompt(String prompt, String conversationId) asyn
}),
);
if (response == null) return '';
- if (response.statusCode == 200){
+ if (response.statusCode == 200) {
return jsonDecode(response.body)['summary'];
- }else {
+ } else {
return '';
}
}
diff --git a/app/lib/backend/http/api/messages.dart b/app/lib/backend/http/api/messages.dart
index d804ab6a0..44bd0723b 100644
--- a/app/lib/backend/http/api/messages.dart
+++ b/app/lib/backend/http/api/messages.dart
@@ -40,7 +40,7 @@ Future> getMessagesServer({
Future> clearChatServer({String? pluginId}) async {
if (pluginId == 'no_selected') pluginId = null;
var response = await makeApiCall(
- url: '${Env.apiBaseUrl}v1/messages?plugin_id=${pluginId ?? ''}',
+ url: '${Env.apiBaseUrl}v2/messages?plugin_id=${pluginId ?? ''}',
headers: {},
method: 'DELETE',
body: '',
@@ -53,32 +53,6 @@ Future> clearChatServer({String? pluginId}) async {
}
}
-Future sendMessageServer(String text, {String? appId, List? fileIds}) {
- var url = '${Env.apiBaseUrl}v1/messages?plugin_id=$appId';
- if (appId == null || appId.isEmpty || appId == 'null' || appId == 'no_selected') {
- url = '${Env.apiBaseUrl}v1/messages';
- }
- return makeApiCall(
- url: url,
- headers: {},
- method: 'POST',
- body: jsonEncode({'text': text, 'file_ids': fileIds}),
- ).then((response) {
- if (response == null) throw Exception('Failed to send message');
- if (response.statusCode == 200) {
- return ServerMessage.fromJson(jsonDecode(response.body));
- } else {
- Logger.error('Failed to send message ${response.body}');
- CrashReporting.reportHandledCrash(
- Exception('Failed to send message ${response.body}'),
- StackTrace.current,
- level: NonFatalExceptionLevel.error,
- );
- return ServerMessage.failedMessage();
- }
- });
-}
-
ServerMessageChunk? parseMessageChunk(String line, String messageId) {
if (line.startsWith('think: ')) {
return ServerMessageChunk(messageId, line.substring(7).replaceAll("__CRLF__", "\n"), MessageChunkType.think);
@@ -164,7 +138,7 @@ Stream sendMessageStreamServer(String text, {String? appId,
Future getInitialAppMessage(String? appId) {
return makeApiCall(
- url: '${Env.apiBaseUrl}v1/initial-message?plugin_id=$appId',
+ url: '${Env.apiBaseUrl}v2/initial-message?app_id=$appId',
headers: {},
method: 'POST',
body: '',
@@ -235,36 +209,10 @@ Stream sendVoiceMessageStreamServer(List files) async*
}
}
-Future> sendVoiceMessageServer(List files) async {
- var request = http.MultipartRequest(
- 'POST',
- Uri.parse('${Env.apiBaseUrl}v1/voice-messages'),
- );
- for (var file in files) {
- request.files.add(await http.MultipartFile.fromPath('files', file.path, filename: basename(file.path)));
- }
- request.headers.addAll({'Authorization': await getAuthHeader()});
-
- try {
- var streamedResponse = await request.send();
- var response = await http.Response.fromStream(streamedResponse);
- if (response.statusCode == 200) {
- debugPrint('sendVoiceMessageServer response body: ${jsonDecode(response.body)}');
- return ((jsonDecode(response.body) ?? []) as List).map((m) => ServerMessage.fromJson(m)).toList();
- } else {
- debugPrint('Failed to upload sample. Status code: ${response.statusCode} ${response.body}');
- throw Exception('Failed to upload sample. Status code: ${response.statusCode}');
- }
- } catch (e) {
- debugPrint('An error occurred uploadSample: $e');
- throw Exception('An error occurred uploadSample: $e');
- }
-}
-
Future?> uploadFilesServer(List files, {String? appId}) async {
- var url = '${Env.apiBaseUrl}v1/files?plugin_id=$appId';
+ var url = '${Env.apiBaseUrl}v2/files?app_id=$appId';
if (appId == null || appId.isEmpty || appId == 'null' || appId == 'no_selected') {
- url = '${Env.apiBaseUrl}v1/files';
+ url = '${Env.apiBaseUrl}v2/files';
}
var request = http.MultipartRequest(
'POST',
@@ -301,7 +249,7 @@ Future?> uploadFilesServer(List files, {String? appId})
Future reportMessageServer(String messageId) async {
var response = await makeApiCall(
- url: '${Env.apiBaseUrl}v1/messages/$messageId/report',
+ url: '${Env.apiBaseUrl}v2/messages/$messageId/report',
headers: {},
method: 'POST',
body: '',
@@ -312,20 +260,19 @@ Future reportMessageServer(String messageId) async {
}
}
-
Future transcribeVoiceMessage(File audioFile) async {
try {
var request = http.MultipartRequest(
'POST',
- Uri.parse('${Env.apiBaseUrl}v1/voice-message/transcribe'),
+ Uri.parse('${Env.apiBaseUrl}v2/voice-message/transcribe'),
);
-
+
request.headers.addAll({'Authorization': await getAuthHeader()});
request.files.add(await http.MultipartFile.fromPath('files', audioFile.path));
-
+
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
-
+
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['transcript'] ?? '';
diff --git a/app/lib/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart
index e2591c229..cebd34a63 100644
--- a/app/lib/backend/http/api/users.dart
+++ b/app/lib/backend/http/api/users.dart
@@ -183,7 +183,6 @@ Future> getAllPeople({bool includeSpeechSamples = true}) async {
body: '',
);
if (response == null) return [];
- debugPrint('getAllPeople response: ${response.body}');
if (response.statusCode == 200) {
List peopleJson = jsonDecode(response.body);
List people = peopleJson.mapIndexed((idx, json) {
diff --git a/app/lib/backend/preferences.dart b/app/lib/backend/preferences.dart
index b6ac3c500..fe0aaf372 100644
--- a/app/lib/backend/preferences.dart
+++ b/app/lib/backend/preferences.dart
@@ -213,6 +213,10 @@ class SharedPreferencesUtil {
set showConversationDeleteConfirmation(bool value) => saveBool("showConversationDeleteConfirmation", value);
+ bool get showActionItemDeleteConfirmation => getBool('showActionItemDeleteConfirmation') ?? true;
+
+ set showActionItemDeleteConfirmation(bool value) => saveBool('showActionItemDeleteConfirmation', value);
+
List get appsList {
final List apps = getStringList('appsList') ?? [];
return App.fromJsonList(apps.map((e) => jsonDecode(e)).toList());
diff --git a/app/lib/backend/schema/conversation.dart b/app/lib/backend/schema/conversation.dart
index 5e3202fb9..ed9fdd575 100644
--- a/app/lib/backend/schema/conversation.dart
+++ b/app/lib/backend/schema/conversation.dart
@@ -220,6 +220,21 @@ class ServerConversation {
return transcript;
}
}
+
+ /// Calculates the conversation duration in seconds based on transcript segments
+ int getDurationInSeconds() {
+ if (transcriptSegments.isEmpty) return 0;
+
+ // Find the last segment's end time
+ double lastEndTime = 0;
+ for (var segment in transcriptSegments) {
+ if (segment.end > lastEndTime) {
+ lastEndTime = segment.end;
+ }
+ }
+
+ return lastEndTime.toInt();
+ }
}
class SyncLocalFilesResponse {
diff --git a/app/lib/backend/schema/memory.dart b/app/lib/backend/schema/memory.dart
index 85ef9caf8..53aee7084 100644
--- a/app/lib/backend/schema/memory.dart
+++ b/app/lib/backend/schema/memory.dart
@@ -1,4 +1,4 @@
-enum MemoryCategory { core, lifestyle, hobbies, interests, habits, work, skills, learnings, other }
+enum MemoryCategory { interesting, system }
enum MemoryVisibility { private, public }
@@ -40,7 +40,7 @@ class Memory {
content: json['content'],
category: MemoryCategory.values.firstWhere(
(e) => e.toString().split('.').last == json['category'],
- orElse: () => MemoryCategory.other,
+ orElse: () => MemoryCategory.interesting,
),
createdAt: DateTime.parse(json['created_at']).toLocal(),
updatedAt: DateTime.parse(json['updated_at']).toLocal(),
@@ -50,9 +50,7 @@ class Memory {
manuallyAdded: json['manually_added'] ?? false,
edited: json['edited'] ?? false,
deleted: json['deleted'] ?? false,
- visibility: json['visibility'] != null
- ? (MemoryVisibility.values.asNameMap()[json['visibility']] ?? MemoryVisibility.public)
- : MemoryVisibility.public,
+ visibility: json['visibility'] != null ? (MemoryVisibility.values.asNameMap()[json['visibility']] ?? MemoryVisibility.public) : MemoryVisibility.public,
);
}
diff --git a/app/lib/pages/action_items/action_items_page.dart b/app/lib/pages/action_items/action_items_page.dart
new file mode 100644
index 000000000..a6fdf3918
--- /dev/null
+++ b/app/lib/pages/action_items/action_items_page.dart
@@ -0,0 +1,354 @@
+import 'package:flutter/material.dart';
+import 'package:omi/providers/conversation_provider.dart';
+import 'package:provider/provider.dart';
+import 'widgets/action_item_title_widget.dart';
+import 'widgets/convo_action_items_group_widget.dart';
+import 'package:omi/backend/schema/conversation.dart';
+import 'package:omi/backend/schema/structured.dart';
+import 'package:omi/utils/analytics/mixpanel.dart';
+
+class ActionItemsPage extends StatefulWidget {
+ const ActionItemsPage({super.key});
+
+ @override
+ State createState() => _ActionItemsPageState();
+}
+
+class _ActionItemsPageState extends State with AutomaticKeepAliveClientMixin {
+ bool _showGroupedView = false;
+
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ MixpanelManager().actionItemsPageOpened();
+ });
+ }
+
+ // Get all action items as a flat list
+ List _getFlattenedActionItems(Map> itemsByConversation) {
+ final result = [];
+
+ for (final entry in itemsByConversation.entries) {
+ for (final item in entry.value) {
+ if (item.deleted) continue;
+ result.add(
+ ActionItemData(
+ actionItem: item,
+ conversation: entry.key,
+ itemIndex: entry.key.structured.actionItems.indexOf(item),
+ ),
+ );
+ }
+ }
+
+ // Sort by completion status (incomplete first)
+ result.sort((a, b) {
+ if (a.actionItem.completed == b.actionItem.completed) return 0;
+ return a.actionItem.completed ? 1 : -1;
+ });
+
+ return result;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Consumer(
+ builder: (context, convoProvider, child) {
+ final Map> itemsByConversation =
+ convoProvider.conversationsWithActiveActionItems;
+
+ // Sort conversations by date (most recent first)
+ final sortedEntries = itemsByConversation.entries.toList()
+ ..sort((a, b) => b.key.createdAt.compareTo(a.key.createdAt));
+
+ // Get flattened list for non-grouped view
+ final flattenedItems = _getFlattenedActionItems(itemsByConversation);
+
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ body: CustomScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ slivers: [
+ // Main Content
+ if (convoProvider.isLoadingConversations && sortedEntries.isEmpty)
+ const SliverFillRemaining(
+ child: Center(
+ child: CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(Colors.white),
+ ),
+ ),
+ )
+ else if (sortedEntries.isEmpty)
+ SliverFillRemaining(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.check_circle_outline,
+ size: 72,
+ color: Colors.grey.shade400,
+ ),
+ const SizedBox(height: 24),
+ Text(
+ 'No Action Items',
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Tasks and to-dos from your conversations will appear here once they are created.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 16,
+ height: 1.5,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ )
+ else
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 0.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'To-Do\'s (${flattenedItems.where((item) => !item.actionItem.completed).length})',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const Row(
+ children: [
+ // Settings button
+ // TODO: Add settings once we have more stuff for action items
+ // Container(
+ // width: 44,
+ // height: 44,
+ // margin: const EdgeInsets.only(right: 8),
+ // decoration: BoxDecoration(
+ // color: Colors.grey.shade700.withOpacity(0.3),
+ // borderRadius: BorderRadius.circular(12),
+ // ),
+ // child: IconButton(
+ // icon: const Icon(Icons.tune, size: 20),
+ // color: Colors.white,
+ // onPressed: () {
+ // // Filter settings
+ // },
+ // ),
+ // ),
+ // Group/Ungroup toggle button hidden for now
+ // Container(
+ // width: 44,
+ // height: 44,
+ // decoration: BoxDecoration(
+ // color: _showGroupedView
+ // ? Colors.deepPurpleAccent.withOpacity(0.3)
+ // : Colors.grey.shade700.withOpacity(0.3),
+ // borderRadius: BorderRadius.circular(12),
+ // border: _showGroupedView
+ // ? Border.all(color: Colors.deepPurpleAccent, width: 1.5)
+ // : null,
+ // ),
+ // child: IconButton(
+ // icon: Icon(
+ // _showGroupedView ? Icons.view_agenda_outlined : Icons.view_list_outlined,
+ // size: 20,
+ // ),
+ // color: _showGroupedView ? Colors.deepPurpleAccent : Colors.white,
+ // onPressed: () {
+ // setState(() {
+ // _showGroupedView = !_showGroupedView;
+ // });
+ // MixpanelManager().actionItemsViewToggled(_showGroupedView);
+ // },
+ // ),
+ // ),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ ),
+
+ if (sortedEntries.isNotEmpty)
+ _showGroupedView ? _buildGroupedView(sortedEntries) : _buildFlatView(flattenedItems),
+
+ if (sortedEntries.isNotEmpty)
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const Text(
+ 'Completed',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.grey[800],
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ '${flattenedItems.where((item) => item.actionItem.completed).length}',
+ style: const TextStyle(
+ color: Colors.grey,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ // Text(
+ // 'Hide',
+ // style: TextStyle(
+ // color: Colors.grey.shade400,
+ // fontSize: 14,
+ // fontWeight: FontWeight.w500,
+ // ),
+ // ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ if (sortedEntries.isNotEmpty && flattenedItems.any((item) => item.actionItem.completed))
+ SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ final completedItems = flattenedItems.where((item) => item.actionItem.completed).toList();
+ if (index < completedItems.length) {
+ final item = completedItems[index];
+ // Simple container for proper spacing
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ child: ActionItemTileWidget(
+ actionItem: item.actionItem,
+ conversationId: item.conversation.id,
+ itemIndexInConversation: item.itemIndex,
+ ),
+ );
+ }
+ return null;
+ },
+ childCount: flattenedItems.where((item) => item.actionItem.completed).length,
+ ),
+ )
+ else if (sortedEntries.isNotEmpty)
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Container(
+ height: 52,
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Center(
+ child: Text(
+ 'No completed items yet',
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildGroupedView(List>> sortedEntries) {
+ return SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ final entry = sortedEntries[index];
+ return ConversationActionItemsGroupWidget(
+ conversation: entry.key,
+ actionItems: entry.value,
+ );
+ },
+ childCount: sortedEntries.length,
+ ),
+ );
+ }
+
+ Widget _buildFlatView(List items) {
+ final incompleteItems = items.where((item) => !item.actionItem.completed).toList();
+
+ return SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ final item = incompleteItems[index];
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ child: ActionItemTileWidget(
+ actionItem: item.actionItem,
+ conversationId: item.conversation.id,
+ itemIndexInConversation: item.itemIndex,
+ ),
+ );
+ },
+ childCount: incompleteItems.length,
+ ),
+ );
+ }
+}
+
+class ActionItemData {
+ final ActionItem actionItem;
+ final ServerConversation conversation;
+ final int itemIndex;
+
+ ActionItemData({
+ required this.actionItem,
+ required this.conversation,
+ required this.itemIndex,
+ });
+}
diff --git a/app/lib/pages/action_items/widgets/action_item_title_widget.dart b/app/lib/pages/action_items/widgets/action_item_title_widget.dart
new file mode 100644
index 000000000..f6aa26198
--- /dev/null
+++ b/app/lib/pages/action_items/widgets/action_item_title_widget.dart
@@ -0,0 +1,312 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:omi/backend/schema/structured.dart';
+import 'package:omi/providers/conversation_provider.dart';
+import 'package:provider/provider.dart';
+import 'package:omi/utils/analytics/mixpanel.dart';
+
+import 'edit_action_item_sheet.dart';
+import 'package:omi/widgets/confirmation_dialog.dart';
+import 'package:omi/backend/preferences.dart';
+
+class ActionItemTileWidget extends StatefulWidget {
+ final ActionItem actionItem;
+ final String conversationId;
+ final int itemIndexInConversation;
+ final bool hasRoundedCorners;
+ final bool isLastInGroup;
+ final bool isInGroup;
+
+ const ActionItemTileWidget({
+ super.key,
+ required this.actionItem,
+ required this.conversationId,
+ required this.itemIndexInConversation,
+ this.hasRoundedCorners = true,
+ this.isLastInGroup = false,
+ this.isInGroup = false,
+ });
+
+ @override
+ State createState() => _ActionItemTileWidgetState();
+}
+
+class _ActionItemTileWidgetState extends State {
+ // Assume SharedPreferencesUtil is available and has these methods:
+ // bool get dontAskAgainDeleteActionItem => _prefs.getBool('dont_ask_again_delete_action_item') ?? false;
+ // Future setDontAskAgainDeleteActionItem(bool value) async => await _prefs.setBool('dont_ask_again_delete_action_item', value);
+
+ @override
+ Widget build(BuildContext context) {
+ final provider = Provider.of(context, listen: false);
+ final conversation = provider.conversations.firstWhere(
+ (c) => c.id == widget.conversationId,
+ orElse: () => provider.searchedConversations.firstWhere((c) => c.id == widget.conversationId),
+ );
+
+ BorderRadius borderRadius;
+ if (widget.hasRoundedCorners) {
+ borderRadius = BorderRadius.circular(16);
+ } else if (widget.isLastInGroup) {
+ borderRadius = const BorderRadius.only(
+ bottomLeft: Radius.circular(16),
+ bottomRight: Radius.circular(16),
+ );
+ } else {
+ borderRadius = BorderRadius.zero;
+ }
+
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: borderRadius,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ // ClipRRect to enforce rounded corners throughout the dismissible animation
+ clipBehavior: Clip.antiAlias,
+ child: Dismissible(
+ key: Key("${widget.conversationId}_${widget.itemIndexInConversation}"),
+ // Allow horizontal swipe in both directions
+ direction: DismissDirection.horizontal,
+
+ // Background for complete action (swipe right, startToEnd)
+ background: Container(
+ alignment: Alignment.centerLeft,
+ color: Colors.green,
+ child: const Padding(
+ padding: EdgeInsets.only(left: 20),
+ child: Row(
+ children: [
+ Icon(
+ Icons.check_circle,
+ color: Colors.white,
+ size: 30,
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Background for delete action (swipe left, endToStart)
+ secondaryBackground: Container(
+ alignment: Alignment.centerRight,
+ color: Colors.red,
+ child: const Padding(
+ padding: EdgeInsets.only(right: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Icon(
+ Icons.delete,
+ color: Colors.white,
+ size: 30,
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ confirmDismiss: (direction) async {
+ if (direction == DismissDirection.endToStart) {
+ final prefsUtil = SharedPreferencesUtil();
+ bool dontAskAgain = !(prefsUtil.showActionItemDeleteConfirmation);
+
+ if (dontAskAgain) {
+ context.read().deleteActionItemAndUpdateLocally(
+ widget.conversationId,
+ widget.itemIndexInConversation,
+ widget.actionItem,
+ );
+ return true;
+ }
+
+ // Delete action (swipe left) - show confirmation dialog
+ return await showDialog(
+ context: context,
+ builder: (context) => ConfirmationDialog(
+ title: 'Delete Action Item',
+ description: 'Are you sure you want to delete this action item?',
+ checkboxText: "Don't ask again",
+ checkboxValue: dontAskAgain,
+ onCheckboxChanged: (value) {
+ prefsUtil.showActionItemDeleteConfirmation = !value;
+ },
+ confirmText: 'Delete',
+ cancelText: 'Cancel',
+ onConfirm: () {
+ context.read().deleteActionItemAndUpdateLocally(
+ widget.conversationId,
+ widget.itemIndexInConversation,
+ widget.actionItem,
+ );
+ Navigator.pop(context, true);
+ },
+ onCancel: () => Navigator.pop(context, false),
+ ),
+ ) ??
+ false;
+ } else if (direction == DismissDirection.startToEnd) {
+ // Complete action (swipe right) - toggle completed state
+ final newValue = !widget.actionItem.completed;
+ context.read().updateGlobalActionItemState(
+ conversation,
+ widget.itemIndexInConversation,
+ newValue,
+ );
+ return false;
+ }
+ return false;
+ },
+
+ onDismissed: (direction) {},
+
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () {
+ HapticFeedback.lightImpact();
+ MixpanelManager().actionItemTappedForEditOnActionItemsPage(
+ conversationId: widget.conversationId,
+ actionItemDescription: widget.actionItem.description,
+ );
+ _showEditActionItemBottomSheet(context, widget.actionItem);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 20,
+ height: 20,
+ child: Transform.translate(
+ offset: const Offset(0, 2),
+ child: GestureDetector(
+ onTap: () {
+ HapticFeedback.lightImpact();
+ final newValue = !widget.actionItem.completed;
+ MixpanelManager().actionItemToggledCompletionOnActionItemsPage(
+ conversationId: widget.conversationId,
+ actionItemDescription: widget.actionItem.description,
+ isCompleted: newValue,
+ );
+ context.read().updateGlobalActionItemState(
+ conversation,
+ widget.itemIndexInConversation,
+ newValue,
+ );
+ },
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ height: 20,
+ width: 20,
+ decoration: BoxDecoration(
+ color: widget.actionItem.completed ? Colors.deepPurpleAccent : Colors.transparent,
+ border: Border.all(
+ color: widget.actionItem.completed ? Colors.deepPurpleAccent : Colors.grey[400]!,
+ width: 1.5,
+ ),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: widget.actionItem.completed
+ ? const Icon(
+ Icons.check,
+ size: 14,
+ color: Colors.white,
+ )
+ : null,
+ ),
+ ),
+ ),
+ ),
+
+ // Content
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(left: 14.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ widget.actionItem.description,
+ style: TextStyle(
+ color: widget.actionItem.completed ? Colors.grey.shade500 : Colors.white,
+ decoration: widget.actionItem.completed ? TextDecoration.lineThrough : null,
+ decorationColor: Colors.grey.shade600,
+ fontSize: 16,
+ height: 1.3,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Optional date/time for tasks
+ if (widget.actionItem.description.toLowerCase().contains('february') ||
+ widget.actionItem.description.toLowerCase().contains('masterclass'))
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.notifications_outlined,
+ size: 14,
+ color: Colors.grey.shade400,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ 'February 28 - 11:00am',
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 13,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showEditActionItemBottomSheet(BuildContext context, ActionItem item) {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (context) {
+ return EditActionItemBottomSheet(
+ actionItem: item,
+ conversationId: widget.conversationId,
+ itemIndex: widget.itemIndexInConversation,
+ );
+ },
+ );
+ }
+}
diff --git a/app/lib/pages/action_items/widgets/convo_action_items_group_widget.dart b/app/lib/pages/action_items/widgets/convo_action_items_group_widget.dart
new file mode 100644
index 000000000..00ad6cca2
--- /dev/null
+++ b/app/lib/pages/action_items/widgets/convo_action_items_group_widget.dart
@@ -0,0 +1,135 @@
+import 'package:flutter/material.dart';
+import 'package:omi/backend/schema/conversation.dart';
+import 'package:omi/backend/schema/structured.dart';
+import 'package:omi/pages/conversation_detail/page.dart';
+import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart';
+import 'package:omi/providers/conversation_provider.dart';
+import 'package:omi/utils/other/temp.dart';
+import 'package:provider/provider.dart';
+
+import 'action_item_title_widget.dart';
+
+class ConversationActionItemsGroupWidget extends StatelessWidget {
+ final ServerConversation conversation;
+ final List actionItems;
+
+ const ConversationActionItemsGroupWidget({
+ super.key,
+ required this.conversation,
+ required this.actionItems,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final sortedItems = [...actionItems]..sort((a, b) {
+ if (a.completed == b.completed) return 0;
+ return a.completed ? 1 : -1;
+ });
+
+ return Container(
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 8,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InkWell(
+ onTap: () {
+ _navigateToConversationDetail(context);
+ },
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ conversation.structured.title.isNotEmpty
+ ? conversation.structured.title
+ : 'Untitled Conversation',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ '${sortedItems.where((item) => !item.completed).length} remaining',
+ style: TextStyle(
+ color: Colors.grey[400],
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Icon(
+ Icons.chevron_right,
+ color: Colors.grey[400],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Action Items List
+ ...sortedItems.asMap().entries.map((entry) {
+ final index = entry.key;
+ final item = entry.value;
+ final isLastItem = index == sortedItems.length - 1;
+
+ return ActionItemTileWidget(
+ actionItem: item,
+ conversationId: conversation.id,
+ itemIndexInConversation: conversation.structured.actionItems.indexOf(item),
+ hasRoundedCorners: false,
+ isLastInGroup: isLastItem,
+ isInGroup: true,
+ );
+ }),
+ const SizedBox(height: 8),
+ ],
+ ),
+ );
+ }
+
+ void _navigateToConversationDetail(BuildContext context) async {
+ final convoProvider = Provider.of(context, listen: false);
+
+ DateTime? date;
+ int? index;
+
+ for (final entry in convoProvider.groupedConversations.entries) {
+ final foundIndex = entry.value.indexWhere((c) => c.id == conversation.id);
+ if (foundIndex != -1) {
+ date = entry.key;
+ index = foundIndex;
+ break;
+ }
+ }
+
+ if (date != null && index != null) {
+ final detailProvider = Provider.of(context, listen: false);
+ detailProvider.updateConversation(index, date);
+
+ convoProvider.onConversationTap(index);
+
+ await routeToPage(
+ context,
+ ConversationDetailPage(conversation: conversation),
+ );
+ }
+ }
+}
diff --git a/app/lib/pages/action_items/widgets/edit_action_item_sheet.dart b/app/lib/pages/action_items/widgets/edit_action_item_sheet.dart
new file mode 100644
index 000000000..8331762b4
--- /dev/null
+++ b/app/lib/pages/action_items/widgets/edit_action_item_sheet.dart
@@ -0,0 +1,252 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:omi/backend/http/api/conversations.dart';
+import 'package:omi/backend/schema/conversation.dart';
+import 'package:omi/backend/schema/structured.dart';
+import 'package:omi/providers/conversation_provider.dart';
+import 'package:provider/provider.dart';
+
+class EditActionItemBottomSheet extends StatefulWidget {
+ final ActionItem actionItem;
+ final String conversationId;
+ final int itemIndex;
+
+ const EditActionItemBottomSheet({
+ super.key,
+ required this.actionItem,
+ required this.conversationId,
+ required this.itemIndex,
+ });
+
+ @override
+ State createState() => _EditActionItemBottomSheetState();
+}
+
+class _EditActionItemBottomSheetState extends State {
+ late TextEditingController _textController;
+ late bool _isCompleted;
+
+ @override
+ void initState() {
+ super.initState();
+ _textController = TextEditingController(text: widget.actionItem.description);
+ _textController.selection = TextSelection.fromPosition(
+ TextPosition(offset: _textController.text.length),
+ );
+ _isCompleted = widget.actionItem.completed;
+ }
+
+ @override
+ void dispose() {
+ _textController.dispose();
+ super.dispose();
+ }
+
+ void _saveActionItem() async {
+ if (_textController.text.trim().isEmpty) {
+ // Optionally, show a message that description can't be empty
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Action item description cannot be empty.'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ return;
+ }
+
+ String oldDescription = widget.actionItem.description;
+ String newDescription = _textController.text.trim();
+
+ updateActionItemDescription(widget.conversationId, oldDescription, newDescription, widget.itemIndex);
+
+ final convoProvider = Provider.of(context, listen: false);
+ convoProvider.updateActionItemDescriptionInConversation(
+ widget.conversationId,
+ widget.itemIndex, // This should be the index of the item in the conversation's list
+ newDescription,
+ );
+
+ if (mounted) {
+ Navigator.pop(context);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.grey.shade900,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // Completed status toggle
+ Row(
+ children: [
+ SizedBox(
+ height: 24,
+ width: 24,
+ child: Checkbox(
+ value: _isCompleted,
+ activeColor: Colors.deepPurpleAccent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(5),
+ ),
+ onChanged: (bool? value) {
+ HapticFeedback.lightImpact();
+ if (value == null) return;
+
+ final convoProvider = Provider.of(context, listen: false);
+ ServerConversation? conversation = convoProvider.conversations.firstWhere(
+ (c) => c.id == widget.conversationId,
+ orElse: () => throw Exception('Conversation not found for ID: ${widget.conversationId}'),
+ );
+
+ convoProvider.updateGlobalActionItemState(
+ conversation,
+ widget.itemIndex,
+ value,
+ );
+
+ setState(() {
+ _isCompleted = value;
+ });
+ },
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ _isCompleted ? 'Completed' : 'Mark complete',
+ style: TextStyle(
+ color: Colors.grey.shade300,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ // Delete button
+ IconButton(
+ icon: const Icon(Icons.delete_outline, color: Colors.red),
+ onPressed: () {
+ // Show delete confirmation dialog
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.grey.shade900,
+ title: const Text(
+ 'Delete Action Item',
+ style: TextStyle(color: Colors.white),
+ ),
+ content: Text(
+ 'Are you sure you want to delete this action item?',
+ style: TextStyle(color: Colors.grey.shade300),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: Text(
+ 'Cancel',
+ style: TextStyle(color: Colors.grey.shade400),
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context, true); // Close dialog
+ Navigator.pop(context); // Close bottom sheet
+
+ // Show confirmation
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Action item deleted'),
+ duration: Duration(seconds: 2),
+ ),
+ );
+ },
+ child: const Text(
+ 'Delete',
+ style: TextStyle(color: Colors.red),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ // Text field for editing the action item
+ TextField(
+ controller: _textController,
+ autofocus: true,
+ maxLines: null,
+ textInputAction: TextInputAction.done,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 16,
+ height: 1.4,
+ ),
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ isDense: true,
+ ),
+ onSubmitted: (value) {
+ if (value.trim().isNotEmpty) {
+ _saveActionItem();
+ }
+ },
+ ),
+ const SizedBox(height: 18),
+ // Bottom row with helper text and character count
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.05),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.keyboard_return,
+ size: 13,
+ color: Colors.grey.shade400,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ 'Press done to save',
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 11,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Text(
+ '${_textController.text.length}/200',
+ style: TextStyle(
+ color: Colors.grey.shade500,
+ fontSize: 11,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart
index 9775d2cf5..161a7c857 100644
--- a/app/lib/pages/apps/app_detail/app_detail.dart
+++ b/app/lib/pages/apps/app_detail/app_detail.dart
@@ -49,7 +49,6 @@ class _AppDetailPageState extends State {
bool isLoading = false;
Timer? _paymentCheckTimer;
late App app;
- late bool showInstallAppConfirmation;
checkSetupCompleted() {
// TODO: move check to backend
@@ -69,7 +68,6 @@ class _AppDetailPageState extends State {
@override
void initState() {
app = widget.app;
- showInstallAppConfirmation = SharedPreferencesUtil().showInstallAppConfirmation;
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Automatically open app home page if conditions are met
if (app.enabled && app.externalIntegration?.appHomeUrl?.isNotEmpty == true) {
@@ -458,14 +456,6 @@ class _AppDetailPageState extends State {
title: 'Data Access Notice',
description:
'This app will access your data. Omi AI is not responsible for how your data is used, modified, or deleted by this app',
- checkboxText: "Don't show it again",
- checkboxValue: !showInstallAppConfirmation,
- onCheckboxChanged: (value) {
- setState(() {
- showInstallAppConfirmation = !value;
- SharedPreferencesUtil().showInstallAppConfirmation = !value;
- });
- },
onConfirm: () {
_toggleApp(app.id, true);
Navigator.pop(context);
diff --git a/app/lib/pages/apps/app_home_web_page.dart b/app/lib/pages/apps/app_home_web_page.dart
index 77c67a747..c9f196150 100644
--- a/app/lib/pages/apps/app_home_web_page.dart
+++ b/app/lib/pages/apps/app_home_web_page.dart
@@ -45,9 +45,11 @@ class _AppHomeWebPageState extends State with SingleTickerProvid
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
- setState(() {
- _isLoading = false;
- });
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
},
onWebResourceError: (WebResourceError error) {
setState(() {
diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart
index 87f8809b9..d2c8a687f 100644
--- a/app/lib/pages/apps/providers/add_app_provider.dart
+++ b/app/lib/pages/apps/providers/add_app_provider.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -663,6 +664,9 @@ class AddAppProvider extends ChangeNotifier {
var file = await imagePicker.pickImage(source: ImageSource.gallery);
if (file != null) {
imageFile = File(file.path);
+ if (imageUrl != null) {
+ await CachedNetworkImage.evictFromCache(imageUrl!, cacheKey: imageUrl);
+ }
imageUrl = null;
}
notifyListeners();
diff --git a/app/lib/pages/apps/widgets/api_keys_widget.dart b/app/lib/pages/apps/widgets/api_keys_widget.dart
index 845c6bab5..310502e98 100644
--- a/app/lib/pages/apps/widgets/api_keys_widget.dart
+++ b/app/lib/pages/apps/widgets/api_keys_widget.dart
@@ -40,9 +40,11 @@ class _ApiKeysWidgetState extends State {
try {
await Provider.of(context, listen: false).loadApiKeys(widget.appId);
} finally {
- setState(() {
- _isLoading = false;
- });
+ if(mounted){
+ setState(() {
+ _isLoading = false;
+ });
+ }
}
}
diff --git a/app/lib/pages/apps/widgets/app_section_card.dart b/app/lib/pages/apps/widgets/app_section_card.dart
index c527ed9a7..7242f9ac6 100644
--- a/app/lib/pages/apps/widgets/app_section_card.dart
+++ b/app/lib/pages/apps/widgets/app_section_card.dart
@@ -7,6 +7,7 @@ import 'package:omi/pages/apps/app_detail/app_detail.dart';
import 'package:omi/providers/app_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
+import 'package:omi/widgets/extensions/string.dart';
import 'package:provider/provider.dart';
class AppSectionCard extends StatelessWidget {
@@ -146,7 +147,7 @@ class SectionAppItemCard extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
- app.getCategoryName(),
+ app.description.decodeString,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.grey, fontSize: 13),
diff --git a/app/lib/pages/apps/widgets/create_options_sheet.dart b/app/lib/pages/apps/widgets/create_options_sheet.dart
index 00141f6df..a65157b54 100644
--- a/app/lib/pages/apps/widgets/create_options_sheet.dart
+++ b/app/lib/pages/apps/widgets/create_options_sheet.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:omi/pages/apps/add_app.dart';
+import 'package:omi/pages/persona/persona_profile.dart';
import 'package:omi/pages/persona/persona_provider.dart';
import 'package:omi/providers/home_provider.dart';
import 'package:provider/provider.dart';
@@ -64,8 +65,16 @@ class CreateOptionsSheet extends StatelessWidget {
MixpanelManager().pageOpened('Create Persona');
// Set routing in provider and navigate to Persona Profile page
Provider.of(context, listen: false).setRouting(PersonaProfileRouting.create_my_clone);
- Provider.of(context, listen: false).setIndex(3);
- Provider.of(context, listen: false).onSelectedIndexChanged!(3);
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => const PersonaProfilePage(),
+ settings: const RouteSettings(
+ arguments: 'from_settings',
+ ),
+ ),
+ );
+ // Provider.of(context, listen: false).setIndex(3);
+ // Provider.of(context, listen: false).onSelectedIndexChanged!(3);
},
),
),
diff --git a/app/lib/pages/apps/widgets/filter_sheet.dart b/app/lib/pages/apps/widgets/filter_sheet.dart
index d097b9fad..27f3288c7 100644
--- a/app/lib/pages/apps/widgets/filter_sheet.dart
+++ b/app/lib/pages/apps/widgets/filter_sheet.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:omi/providers/app_provider.dart';
+import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:provider/provider.dart';
class FilterBottomSheet extends StatelessWidget {
@@ -50,6 +51,8 @@ class FilterBottomSheet extends StatelessWidget {
label: 'Installed Apps',
onTap: () {
provider.addOrRemoveFilter('Installed Apps', 'Apps');
+ MixpanelManager().appsTypeFilter(
+ 'Installed Apps', provider.isFilterSelected('Installed Apps', 'Apps'));
},
isSelected: provider.isFilterSelected('Installed Apps', 'Apps'),
),
@@ -57,6 +60,7 @@ class FilterBottomSheet extends StatelessWidget {
label: 'My Apps',
onTap: () {
provider.addOrRemoveFilter('My Apps', 'Apps');
+ MixpanelManager().appsTypeFilter('My Apps', provider.isFilterSelected('My Apps', 'Apps'));
},
isSelected: provider.isFilterSelected('My Apps', 'Apps'),
),
@@ -71,6 +75,7 @@ class FilterBottomSheet extends StatelessWidget {
label: 'A-Z',
onTap: () {
provider.addOrRemoveFilter('A-Z', 'Sort');
+ MixpanelManager().appsSortFilter('A-Z', provider.isFilterSelected('A-Z', 'Sort'));
},
isSelected: provider.isFilterSelected('A-Z', 'Sort'),
),
@@ -78,6 +83,7 @@ class FilterBottomSheet extends StatelessWidget {
label: 'Z-A',
onTap: () {
provider.addOrRemoveFilter('Z-A', 'Sort');
+ MixpanelManager().appsSortFilter('Z-A', provider.isFilterSelected('Z-A', 'Sort'));
},
isSelected: provider.isFilterSelected('Z-A', 'Sort'),
),
@@ -85,6 +91,8 @@ class FilterBottomSheet extends StatelessWidget {
label: 'Highest Rating',
onTap: () {
provider.addOrRemoveFilter('Highest Rating', 'Sort');
+ MixpanelManager().appsSortFilter(
+ 'Highest Rating', provider.isFilterSelected('Highest Rating', 'Sort'));
},
isSelected: provider.isFilterSelected('Highest Rating', 'Sort'),
),
@@ -92,6 +100,8 @@ class FilterBottomSheet extends StatelessWidget {
label: 'Lowest Rating',
onTap: () {
provider.addOrRemoveFilter('Lowest Rating', 'Sort');
+ MixpanelManager()
+ .appsSortFilter('Lowest Rating', provider.isFilterSelected('Lowest Rating', 'Sort'));
},
isSelected: provider.isFilterSelected('Lowest Rating', 'Sort'),
),
@@ -106,6 +116,8 @@ class FilterBottomSheet extends StatelessWidget {
label: category.title,
onTap: () {
provider.addOrRemoveCategoryFilter(category);
+ MixpanelManager().appsCategoryFilter(
+ category.title, provider.isCategoryFilterSelected(category));
},
isSelected: provider.isCategoryFilterSelected(category),
))
@@ -120,24 +132,32 @@ class FilterBottomSheet extends StatelessWidget {
label: '1+ Stars',
onTap: () {
provider.addOrRemoveFilter('1+ Stars', 'Rating');
+ MixpanelManager()
+ .appsRatingFilter('1+ Stars', provider.isFilterSelected('1+ Stars', 'Rating'));
},
isSelected: provider.isFilterSelected('1+ Stars', 'Rating')),
FilterOption(
label: '2+ Stars',
onTap: () {
provider.addOrRemoveFilter('2+ Stars', 'Rating');
+ MixpanelManager()
+ .appsRatingFilter('2+ Stars', provider.isFilterSelected('2+ Stars', 'Rating'));
},
isSelected: provider.isFilterSelected('2+ Stars', 'Rating')),
FilterOption(
label: '3+ Stars',
onTap: () {
provider.addOrRemoveFilter('3+ Stars', 'Rating');
+ MixpanelManager()
+ .appsRatingFilter('3+ Stars', provider.isFilterSelected('3+ Stars', 'Rating'));
},
isSelected: provider.isFilterSelected('3+ Stars', 'Rating')),
FilterOption(
label: '4+ Stars',
onTap: () {
provider.addOrRemoveFilter('4+ Stars', 'Rating');
+ MixpanelManager()
+ .appsRatingFilter('4+ Stars', provider.isFilterSelected('4+ Stars', 'Rating'));
},
isSelected: provider.isFilterSelected('4+ Stars', 'Rating')),
],
@@ -151,6 +171,8 @@ class FilterBottomSheet extends StatelessWidget {
label: capability.title,
onTap: () {
provider.addOrRemoveCapabilityFilter(capability);
+ MixpanelManager().appsCapabilityFilter(
+ capability.title, provider.isCapabilityFilterSelected(capability));
},
isSelected: provider.isCapabilityFilterSelected(capability),
))
@@ -167,6 +189,7 @@ class FilterBottomSheet extends StatelessWidget {
child: ElevatedButton(
onPressed: () {
provider.clearFilters();
+ MixpanelManager().appsClearFilters();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
diff --git a/app/lib/pages/chat/clone_chat_page.dart b/app/lib/pages/chat/clone_chat_page.dart
index cbe624e28..f1752e556 100644
--- a/app/lib/pages/chat/clone_chat_page.dart
+++ b/app/lib/pages/chat/clone_chat_page.dart
@@ -32,6 +32,10 @@ class CloneChatPageState extends State {
if (provider.userPersona != null) {
App selectedApp = provider.userPersona!;
+ if (!mounted){
+ return;
+ }
+
var appProvider = Provider.of(context, listen: false);
SharedPreferencesUtil().appsList = [selectedApp];
appProvider.setApps();
diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart
index 3b8a1b724..2e3acbd54 100644
--- a/app/lib/pages/chat/page.dart
+++ b/app/lib/pages/chat/page.dart
@@ -84,6 +84,10 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin {
}
});
SchedulerBinding.instance.addPostFrameCallback((_) async {
+ var provider = context.read();
+ if (provider.messages.isEmpty) {
+ provider.refreshMessages();
+ }
scrollToBottom();
});
super.initState();
@@ -499,6 +503,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin {
setState(() {
textController.text = transcript;
_showVoiceRecorder = false;
+ context.read().setNextMessageOriginIsVoice(true);
});
},
onClose: () {
@@ -518,12 +523,12 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin {
focusNode: home.chatFieldFocusNode,
textAlign: TextAlign.start,
textAlignVertical: TextAlignVertical.top,
- decoration: InputDecoration(
+ decoration: const InputDecoration(
hintText: 'Message',
- hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey),
+ hintStyle: TextStyle(fontSize: 14.0, color: Colors.grey),
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
- contentPadding: const EdgeInsets.only(top: 8, bottom: 10),
+ contentPadding: EdgeInsets.only(top: 8, bottom: 10),
),
maxLines: null,
keyboardType: TextInputType.multiline,
@@ -600,7 +605,6 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin {
_sendMessageUtil(String text) {
var provider = context.read();
- MixpanelManager().chatMessageSent(text);
provider.setSendingMessage(true);
provider.addMessageLocally(text);
scrollToBottom();
diff --git a/app/lib/pages/conversation_detail/page.dart b/app/lib/pages/conversation_detail/page.dart
index 4167d06c7..c6a2e0578 100644
--- a/app/lib/pages/conversation_detail/page.dart
+++ b/app/lib/pages/conversation_detail/page.dart
@@ -10,7 +10,6 @@ import 'package:omi/pages/conversation_detail/widgets.dart';
import 'package:omi/pages/settings/people.dart';
import 'package:omi/providers/connectivity_provider.dart';
import 'package:omi/providers/conversation_provider.dart';
-import 'package:omi/utils/alerts/app_snackbar.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
import 'package:omi/widgets/conversation_bottom_bar.dart';
@@ -49,10 +48,23 @@ class _ConversationDetailPageState extends State with Ti
void initState() {
super.initState();
- _controller = TabController(length: 2, vsync: this, initialIndex: 1); // Start with summary tab
+ _controller = TabController(length: 3, vsync: this, initialIndex: 1); // Start with summary tab
_controller!.addListener(() {
setState(() {
- selectedTab = _controller!.index == 0 ? ConversationTab.transcript : ConversationTab.summary;
+ switch (_controller!.index) {
+ case 0:
+ selectedTab = ConversationTab.transcript;
+ break;
+ case 1:
+ selectedTab = ConversationTab.summary;
+ break;
+ case 2:
+ selectedTab = ConversationTab.actionItems;
+ break;
+ default:
+ debugPrint('Invalid tab index: ${_controller!.index}');
+ selectedTab = ConversationTab.summary;
+ }
});
});
@@ -188,6 +200,7 @@ class _ConversationDetailPageState extends State with Ti
},
),
const SummaryTab(),
+ const ActionItemsTab(),
],
);
}),
@@ -210,7 +223,21 @@ class _ConversationDetailPageState extends State with Ti
hasSegments:
conversation.transcriptSegments.isNotEmpty || conversation.externalIntegration != null,
onTabSelected: (tab) {
- int index = tab == ConversationTab.transcript ? 0 : 1;
+ int index;
+ switch (tab) {
+ case ConversationTab.transcript:
+ index = 0;
+ break;
+ case ConversationTab.summary:
+ index = 1;
+ break;
+ case ConversationTab.actionItems:
+ index = 2;
+ break;
+ default:
+ debugPrint('Invalid tab selected: $tab');
+ index = 1; // Default to summary tab
+ }
_controller!.animateTo(index);
},
onStopPressed: () {
@@ -556,3 +583,62 @@ class EditSegmentWidget extends StatelessWidget {
});
}
}
+
+class ActionItemsTab extends StatelessWidget {
+ const ActionItemsTab({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () => FocusScope.of(context).unfocus(),
+ child: Consumer(
+ builder: (context, provider, child) {
+ final hasActionItems = provider.conversation.structured.actionItems.where((item) => !item.deleted).isNotEmpty;
+
+ return ListView(
+ shrinkWrap: true,
+ children: [
+ const SizedBox(height: 24),
+ if (hasActionItems) const ActionItemsListWidget() else _buildEmptyState(context),
+ const SizedBox(height: 150)
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildEmptyState(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.check_circle_outline,
+ size: 72,
+ color: Colors.grey,
+ ),
+ const SizedBox(height: 24),
+ Text(
+ 'No Action Items',
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'This memory doesn\'t have any action items yet. They\'ll appear here when your conversations include tasks or to-dos.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 16,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/pages/conversation_detail/widgets.dart b/app/lib/pages/conversation_detail/widgets.dart
index a7df53d0e..f75dd9a5e 100644
--- a/app/lib/pages/conversation_detail/widgets.dart
+++ b/app/lib/pages/conversation_detail/widgets.dart
@@ -21,6 +21,7 @@ import 'package:omi/providers/connectivity_provider.dart';
import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
+import 'package:omi/utils/other/time_utils.dart';
import 'package:omi/widgets/dialog.dart';
import 'package:omi/widgets/extensions/string.dart';
import 'package:provider/provider.dart';
@@ -42,6 +43,15 @@ class GetSummaryWidgets extends StatelessWidget {
return startedAt == null ? dateTimeFormat('h:mm a', createdAt) : dateTimeFormat('h:mm a', startedAt);
}
+ String _getDuration(ServerConversation conversation) {
+ if (conversation.transcriptSegments.isEmpty) return '';
+
+ int durationSeconds = conversation.getDurationInSeconds();
+ if (durationSeconds <= 0) return '';
+
+ return secondsToHumanReadable(durationSeconds);
+ }
+
@override
Widget build(BuildContext context) {
return Selector>(
@@ -72,6 +82,20 @@ class GetSummaryWidgets extends StatelessWidget {
: '${dateTimeFormat('MMM d, yyyy', conversation.createdAt)} ${conversation.startedAt == null ? 'at' : 'from'} ${setTime(conversation.startedAt, conversation.createdAt, conversation.finishedAt)}',
style: const TextStyle(color: Colors.grey, fontSize: 16),
),
+ if (conversation.transcriptSegments.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 4.0),
+ child: Row(
+ children: [
+ const Icon(Icons.access_time, color: Colors.grey, size: 14),
+ const SizedBox(width: 4),
+ Text(
+ 'Duration: ${_getDuration(conversation)}',
+ style: const TextStyle(color: Colors.grey, fontSize: 14),
+ ),
+ ],
+ ),
+ ),
const SizedBox(height: 16),
conversation.discarded ? const SizedBox.shrink() : const SizedBox(height: 8),
],
@@ -134,27 +158,29 @@ class ActionItemsListWidget extends StatelessWidget {
var tempItem = provider.conversation.structured.actionItems[idx];
var tempIdx = idx;
provider.deleteActionItem(idx);
- ScaffoldMessenger.of(context)
- .showSnackBar(
- SnackBar(
- content: const Text('Action Item deleted successfully 🗑️'),
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
- action: SnackBarAction(
- label: 'Undo',
- textColor: Colors.white,
- onPressed: () {
- provider.undoDeleteActionItem(idx);
- },
- ),
- ),
- )
- .closed
- .then((reason) {
- if (reason != SnackBarClosedReason.action) {
- provider.deleteActionItemPermanently(tempItem, tempIdx);
- MixpanelManager().deletedActionItem(provider.conversation);
- }
- });
+ provider.deleteActionItemPermanently(tempItem, tempIdx);
+ MixpanelManager().deletedActionItem(provider.conversation);
+ // ScaffoldMessenger.of(context)
+ // .showSnackBar(
+ // SnackBar(
+ // content: const Text('Action Item deleted successfully 🗑️'),
+ // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
+ // action: SnackBarAction(
+ // label: 'Undo',
+ // textColor: Colors.white,
+ // onPressed: () {
+ // provider.undoDeleteActionItem(idx);
+ // },
+ // ),
+ // ),
+ // )
+ // .closed
+ // .then((reason) {
+ // if (reason != SnackBarClosedReason.action) {
+ // provider.deleteActionItemPermanently(tempItem, tempIdx);
+ // MixpanelManager().deletedActionItem(provider.conversation);
+ // }
+ // });
},
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 2),
@@ -878,7 +904,7 @@ class _GetShareOptionsState extends State {
changeLoadingShareTranscript(true);
String content = '''
${widget.conversation.structured.title}
-
+
${widget.conversation.getTranscript(generate: true)}
'''
.replaceAll(' ', '')
diff --git a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart
index c51903acb..61b73623d 100644
--- a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart
+++ b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart
@@ -24,6 +24,12 @@ class SummarizedAppsBottomSheet extends StatelessWidget {
builder: (context, provider, _) {
final summarizedApp = provider.getSummarizedApp();
final currentAppId = summarizedApp?.appId;
+ final conversationId = provider.conversation.id;
+
+ MixpanelManager().summarizedAppSheetViewed(
+ conversationId: conversationId,
+ currentSummarizedAppId: currentAppId,
+ );
return _SheetContainer(
scrollController: scrollController,
@@ -146,6 +152,15 @@ class _AppsList extends StatelessWidget {
void _handleAutoAppTap(BuildContext context) async {
Navigator.pop(context);
final provider = context.read();
+ final previousAppId = provider.getSummarizedApp()?.appId;
+ final conversationId = provider.conversation.id;
+
+ MixpanelManager().summarizedAppSelected(
+ conversationId: conversationId,
+ selectedAppId: 'auto',
+ previousAppId: previousAppId,
+ );
+
provider.clearSelectedAppForReprocessing();
await provider.reprocessConversation();
return;
@@ -153,9 +168,18 @@ class _AppsList extends StatelessWidget {
void _handleAppTap(BuildContext context, App app) async {
// If this is a different app than currently selected, reprocess with this app
+ final provider = context.read();
+ final previousAppId = provider.getSummarizedApp()?.appId;
+ final conversationId = provider.conversation.id;
+
if (app.id != currentAppId) {
+ MixpanelManager().summarizedAppSelected(
+ conversationId: conversationId,
+ selectedAppId: app.id,
+ previousAppId: previousAppId,
+ );
+
Navigator.pop(context);
- final provider = context.read();
provider.setSelectedAppForReprocessing(app);
provider.setPreferredSummarizationApp(app.id);
await provider.reprocessConversation(appId: app.id);
@@ -277,6 +301,10 @@ class _EnableAppsListItem extends StatelessWidget {
trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16),
onTap: () {
Navigator.pop(context);
+ final conversationId = context.read().conversation?.id;
+ if (conversationId != null) {
+ MixpanelManager().summarizedAppEnableAppsClicked(conversationId: conversationId);
+ }
routeToPage(context, const AppsPage(showAppBar: true));
MixpanelManager().pageOpened('Detail Apps');
},
diff --git a/app/lib/pages/conversations/conversations_page.dart b/app/lib/pages/conversations/conversations_page.dart
index 05a7603b4..7ec0535b4 100644
--- a/app/lib/pages/conversations/conversations_page.dart
+++ b/app/lib/pages/conversations/conversations_page.dart
@@ -4,6 +4,7 @@ import 'package:omi/pages/capture/widgets/widgets.dart';
import 'package:omi/pages/conversations/widgets/processing_capture.dart';
import 'package:omi/pages/conversations/widgets/search_result_header_widget.dart';
import 'package:omi/pages/conversations/widgets/search_widget.dart';
+import 'package:omi/providers/capture_provider.dart';
import 'package:omi/providers/conversation_provider.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -43,7 +44,9 @@ class _ConversationsPageState extends State with AutomaticKee
backgroundColor: Colors.black,
color: Colors.white,
onRefresh: () async {
- return await convoProvider.getInitialConversations();
+ Provider.of(context, listen: false).refreshInProgressConversations();
+ await convoProvider.getInitialConversations();
+ return;
},
child: CustomScrollView(
slivers: [
diff --git a/app/lib/pages/conversations/widgets/conversation_list_item.dart b/app/lib/pages/conversations/widgets/conversation_list_item.dart
index 5510e6046..31f7bb4f1 100644
--- a/app/lib/pages/conversations/widgets/conversation_list_item.dart
+++ b/app/lib/pages/conversations/widgets/conversation_list_item.dart
@@ -10,6 +10,7 @@ import 'package:omi/providers/connectivity_provider.dart';
import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
+import 'package:omi/utils/other/time_utils.dart';
import 'package:omi/widgets/confirmation_dialog.dart';
import 'package:omi/widgets/dialog.dart';
import 'package:omi/widgets/extensions/string.dart';
@@ -83,8 +84,7 @@ class _ConversationListItemState extends State {
}
},
child: Padding(
- padding:
- EdgeInsets.only(top: 12, left: widget.isFromOnboarding ? 0 : 16, right: widget.isFromOnboarding ? 0 : 16),
+ padding: EdgeInsets.only(top: 12, left: widget.isFromOnboarding ? 0 : 16, right: widget.isFromOnboarding ? 0 : 16),
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
@@ -114,8 +114,7 @@ class _ConversationListItemState extends State {
builder: (context, setState) {
return ConfirmationDialog(
title: "Delete Conversation?",
- description:
- "Are you sure you want to delete this conversation? This action cannot be undone.",
+ description: "Are you sure you want to delete this conversation? This action cannot be undone.",
checkboxValue: !showDeleteConfirmation,
checkboxText: "Don't ask me again",
onCheckboxChanged: (value) {
@@ -134,9 +133,7 @@ class _ConversationListItemState extends State {
});
} else {
return showDialog(
- builder: (c) => getDialog(context, () => Navigator.pop(context), () => Navigator.pop(context),
- 'Unable to Delete Conversation', 'Please check your internet connection and try again.',
- singleButton: true, okButtonText: 'OK'),
+ builder: (c) => getDialog(context, () => Navigator.pop(context), () => Navigator.pop(context), 'Unable to Delete Conversation', 'Please check your internet connection and try again.', singleButton: true, okButtonText: 'OK'),
context: context,
);
}
@@ -166,19 +163,13 @@ class _ConversationListItemState extends State {
? const SizedBox.shrink()
: Text(
structured.overview.decodeString,
- style: Theme.of(context)
- .textTheme
- .bodyMedium!
- .copyWith(color: Colors.grey.shade300, height: 1.3),
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.grey.shade300, height: 1.3),
maxLines: 2,
),
widget.conversation.discarded
? Text(
widget.conversation.getTranscript(maxCount: 100),
- style: Theme.of(context)
- .textTheme
- .bodyMedium!
- .copyWith(color: Colors.grey.shade300, height: 1.3),
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.grey.shade300, height: 1.3),
)
: const SizedBox(height: 8),
],
@@ -196,50 +187,91 @@ class _ConversationListItemState extends State {
return Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12),
child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- widget.conversation.discarded
- ? const SizedBox.shrink()
- : Text(widget.conversation.structured.getEmoji(),
- style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w500)),
- widget.conversation.structured.category.isNotEmpty && !widget.conversation.discarded
- ? const SizedBox(width: 12)
- : const SizedBox.shrink(),
- widget.conversation.structured.category.isNotEmpty
- ? Container(
- decoration: BoxDecoration(
- color: widget.conversation.getTagColor(),
- borderRadius: BorderRadius.circular(16),
+ // 🧠 Emoji + Tag
+ Flexible(
+ fit: FlexFit.tight,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (!widget.conversation.discarded)
+ Text(
+ widget.conversation.structured.getEmoji(),
+ style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w500),
),
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
- child: Text(
- widget.conversation.getTag(),
- style:
- Theme.of(context).textTheme.bodyMedium!.copyWith(color: widget.conversation.getTagTextColor()),
- maxLines: 1,
+ if (widget.conversation.structured.category.isNotEmpty && !widget.conversation.discarded) const SizedBox(width: 8),
+ if (widget.conversation.structured.category.isNotEmpty)
+ Flexible(
+ child: Container(
+ decoration: BoxDecoration(
+ color: widget.conversation.getTagColor(),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ child: Text(
+ widget.conversation.getTag(),
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: widget.conversation.getTagTextColor()),
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ ),
+ ),
),
- )
- : const SizedBox.shrink(),
- const SizedBox(
- width: 16,
+ ],
+ ),
),
- Expanded(
+
+ const SizedBox(width: 12),
+
+ // 🕒 Timestamp + Duration or New
+ FittedBox(
+ fit: BoxFit.scaleDown,
child: isNew
- ? const Align(
- alignment: Alignment.centerRight,
- child: ConversationNewStatusIndicator(text: "New 🚀"),
- )
- : Text(
- dateTimeFormat('MMM d, h:mm a', widget.conversation.startedAt ?? widget.conversation.createdAt),
- style: TextStyle(color: Colors.grey.shade400, fontSize: 14),
- maxLines: 1,
- textAlign: TextAlign.end,
+ ? const ConversationNewStatusIndicator(text: "New 🚀")
+ : Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ dateTimeFormat(
+ 'MMM d, h:mm a',
+ widget.conversation.startedAt ?? widget.conversation.createdAt,
+ ),
+ style: TextStyle(color: Colors.grey.shade400, fontSize: 14),
+ maxLines: 1,
+ ),
+ if (widget.conversation.transcriptSegments.isNotEmpty && _getConversationDuration().isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ _getConversationDuration(),
+ style: TextStyle(color: Colors.grey.shade300, fontSize: 11),
+ maxLines: 1,
+ ),
+ ),
+ ),
+ ],
),
- )
+ ),
],
),
);
}
+
+ String _getConversationDuration() {
+ if (widget.conversation.transcriptSegments.isEmpty) return '';
+
+ // Get the total duration in seconds
+ int durationSeconds = widget.conversation.getDurationInSeconds();
+ if (durationSeconds <= 0) return '';
+
+ return secondsToCompactDuration(durationSeconds);
+ }
}
class ConversationNewStatusIndicator extends StatefulWidget {
@@ -251,8 +283,7 @@ class ConversationNewStatusIndicator extends StatefulWidget {
State createState() => _ConversationNewStatusIndicatorState();
}
-class _ConversationNewStatusIndicatorState extends State
- with SingleTickerProviderStateMixin {
+class _ConversationNewStatusIndicatorState extends State with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _opacityAnim;
diff --git a/app/lib/pages/conversations/widgets/synced_conversation_list_item.dart b/app/lib/pages/conversations/widgets/synced_conversation_list_item.dart
index 597133da0..a5419c7a0 100644
--- a/app/lib/pages/conversations/widgets/synced_conversation_list_item.dart
+++ b/app/lib/pages/conversations/widgets/synced_conversation_list_item.dart
@@ -5,6 +5,7 @@ import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart'
import 'package:omi/pages/conversation_detail/page.dart';
import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/utils/other/temp.dart';
+import 'package:omi/utils/other/time_utils.dart';
import 'package:omi/widgets/extensions/string.dart';
import 'package:provider/provider.dart';
@@ -174,15 +175,47 @@ class _SyncedConversationListItemState extends State
width: 16,
),
Expanded(
- child: Text(
- dateTimeFormat('MMM d, h:mm a', conversation.startedAt ?? conversation.createdAt),
- style: TextStyle(color: Colors.grey.shade400, fontSize: 14),
- maxLines: 1,
- textAlign: TextAlign.end,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ dateTimeFormat('MMM d, h:mm a', conversation.startedAt ?? conversation.createdAt),
+ style: TextStyle(color: Colors.grey.shade400, fontSize: 14),
+ maxLines: 1,
+ textAlign: TextAlign.end,
+ ),
+ if (conversation.transcriptSegments.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 2.0),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ _getConversationDuration(),
+ style: TextStyle(color: Colors.grey.shade300, fontSize: 11),
+ maxLines: 1,
+ textAlign: TextAlign.end,
+ ),
+ ),
+ ),
+ ],
),
)
],
),
);
}
+
+ String _getConversationDuration() {
+ if (conversation.transcriptSegments.isEmpty) return '';
+
+ // Get the total duration in seconds
+ int durationSeconds = conversation.getDurationInSeconds();
+ if (durationSeconds <= 0) return '';
+
+ return secondsToCompactDuration(durationSeconds);
+ }
}
diff --git a/app/lib/pages/home/firmware_update.dart b/app/lib/pages/home/firmware_update.dart
index 19807c5ac..2e311ab54 100644
--- a/app/lib/pages/home/firmware_update.dart
+++ b/app/lib/pages/home/firmware_update.dart
@@ -242,8 +242,8 @@ class _FirmwareUpdateState extends State with FirmwareMixin {
}
Widget _buildUpdateSection() {
- bool hasChangelog = latestFirmwareDetails['changelog'] != null &&
- (List.from(latestFirmwareDetails['changelog'])).isNotEmpty;
+ dynamic changelogData = latestFirmwareDetails['changelog'];
+ bool hasChangelog = changelogData != null && changelogData is List && (List.from(changelogData)).isNotEmpty;
return Card(
color: Colors.black.withOpacity(0.2),
@@ -340,7 +340,7 @@ class _FirmwareUpdateState extends State with FirmwareMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- ...(List.from(latestFirmwareDetails['changelog'])).map((change) => Padding(
+ ...(List.from(changelogData)).map((change) => Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart
index 4ae6ab58d..668ffe76f 100644
--- a/app/lib/pages/home/page.dart
+++ b/app/lib/pages/home/page.dart
@@ -2,29 +2,30 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
-import 'package:omi/gen/assets.gen.dart';
-import 'package:omi/pages/persona/persona_provider.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:gradient_borders/gradient_borders.dart';
+import 'package:instabug_flutter/instabug_flutter.dart';
import 'package:omi/backend/http/api/users.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/app.dart';
import 'package:omi/backend/schema/geolocation.dart';
+import 'package:omi/gen/assets.gen.dart';
import 'package:omi/main.dart';
+import 'package:omi/pages/action_items/action_items_page.dart';
import 'package:omi/pages/apps/page.dart';
import 'package:omi/pages/chat/page.dart';
import 'package:omi/pages/conversations/conversations_page.dart';
-import 'package:omi/pages/memories/page.dart';
import 'package:omi/pages/home/widgets/chat_apps_dropdown_widget.dart';
-import 'package:omi/pages/persona/persona_profile.dart';
-import 'package:omi/pages/home/widgets/speech_language_sheet.dart';
+import 'package:omi/pages/memories/page.dart';
import 'package:omi/pages/settings/page.dart';
import 'package:omi/providers/app_provider.dart';
import 'package:omi/providers/capture_provider.dart';
import 'package:omi/providers/connectivity_provider.dart';
+import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/providers/device_provider.dart';
import 'package:omi/providers/home_provider.dart';
-import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/providers/message_provider.dart';
import 'package:omi/services/notifications.dart';
import 'package:omi/utils/analytics/analytics_manager.dart';
@@ -32,8 +33,6 @@ import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/audio/foreground.dart';
import 'package:omi/utils/other/temp.dart';
import 'package:omi/widgets/upgrade_alert.dart';
-import 'package:gradient_borders/gradient_borders.dart';
-import 'package:instabug_flutter/instabug_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:upgrader/upgrader.dart';
@@ -119,8 +118,8 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
// Reload convos
if (mounted) {
- debugPrint('Reload convos');
- Provider.of(context, listen: false).fetchNewConversations();
+ Provider.of(context, listen: false).refreshConversations();
+ Provider.of(context, listen: false).refreshInProgressConversations();
}
} else if (state == AppLifecycleState.hidden) {
event = 'App is hidden';
@@ -182,13 +181,16 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
break;
case "chat":
homePageIdx = 1;
- case "apps":
+ case "memoriesPage":
homePageIdx = 2;
break;
+ case "apps":
+ homePageIdx = 3;
+ break;
}
}
- // Home controler
+ // Home controller
_controller = PageController(initialPage: homePageIdx);
context.read().selectedIndex = homePageIdx;
context.read().onSelectedIndexChanged = (index) {
@@ -355,9 +357,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
builder: (context, homeProvider, _) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
- appBar: homeProvider.selectedIndex == 3 ? null : _buildAppBar(context),
+ appBar: homeProvider.selectedIndex == 5 ? null : _buildAppBar(context),
body: DefaultTabController(
- length: 3,
+ length: 5,
initialIndex: _controller?.initialPage ?? 0,
child: GestureDetector(
onTap: () {
@@ -374,25 +376,28 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
children: const [
ConversationsPage(),
ChatPage(isPivotBottom: false),
+ MemoriesPage(),
+ ActionItemsPage(),
AppsPage(),
- PersonaProfilePage(bottomMargin: 120),
],
),
),
Consumer(
builder: (context, home, child) {
- if (home.chatFieldFocusNode.hasFocus ||
- home.convoSearchFieldFocusNode.hasFocus ||
- home.appsSearchFieldFocusNode.hasFocus) {
+ if (home.isChatFieldFocused ||
+ home.isConvoSearchFieldFocused ||
+ home.isAppsSearchFieldFocused ||
+ home.isMemoriesSearchFieldFocused) {
return const SizedBox.shrink();
} else {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10),
margin: const EdgeInsets.fromLTRB(20, 16, 20, 42),
decoration: const BoxDecoration(
color: Colors.black,
- borderRadius: BorderRadius.all(Radius.circular(16)),
+ borderRadius: BorderRadius.all(Radius.circular(18)),
border: GradientBoxBorder(
gradient: LinearGradient(colors: [
Color.fromARGB(127, 208, 208, 208),
@@ -405,11 +410,11 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
shape: BoxShape.rectangle,
),
child: TabBar(
- labelPadding: const EdgeInsets.only(top: 4, bottom: 4),
+ labelPadding: const EdgeInsets.symmetric(vertical: 10),
indicatorPadding: EdgeInsets.zero,
onTap: (index) {
- MixpanelManager()
- .bottomNavigationTabClicked(['Memories', 'Chat', 'Explore'][index]);
+ MixpanelManager().bottomNavigationTabClicked(
+ ['Memories', 'Chat', 'Facts', 'Action Items', 'Explore'][index]);
primaryFocus?.unfocus();
if (home.selectedIndex == index) {
return;
@@ -421,30 +426,103 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
indicatorColor: Colors.transparent,
tabs: [
Tab(
- child: Text(
- 'Home',
- style: TextStyle(
- color: home.selectedIndex == 0 ? Colors.white : Colors.grey,
- fontSize: MediaQuery.sizeOf(context).width < 410 ? 13 : 15,
- ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ FontAwesomeIcons.house,
+ color: home.selectedIndex == 0 ? Colors.white : Colors.grey,
+ size: 18,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Home',
+ style: TextStyle(
+ color: home.selectedIndex == 0 ? Colors.white : Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Tab(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ FontAwesomeIcons.solidMessage,
+ color: home.selectedIndex == 1 ? Colors.white : Colors.grey,
+ size: 18,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Chat',
+ style: TextStyle(
+ color: home.selectedIndex == 1 ? Colors.white : Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Tab(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ FontAwesomeIcons.brain,
+ color: home.selectedIndex == 2 ? Colors.white : Colors.grey,
+ size: 18,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Memories',
+ style: TextStyle(
+ color: home.selectedIndex == 2 ? Colors.white : Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
),
),
Tab(
- child: Text(
- 'Chat',
- style: TextStyle(
- color: home.selectedIndex == 1 ? Colors.white : Colors.grey,
- fontSize: MediaQuery.sizeOf(context).width < 410 ? 13 : 15,
- ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ FontAwesomeIcons.listCheck,
+ color: home.selectedIndex == 3 ? Colors.white : Colors.grey,
+ size: 18,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Actions',
+ style: TextStyle(
+ color: home.selectedIndex == 3 ? Colors.white : Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
),
),
Tab(
- child: Text(
- 'Explore',
- style: TextStyle(
- color: home.selectedIndex == 2 ? Colors.white : Colors.grey,
- fontSize: MediaQuery.sizeOf(context).width < 410 ? 13 : 15,
- ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ FontAwesomeIcons.search,
+ color: home.selectedIndex == 4 ? Colors.white : Colors.grey,
+ size: 18,
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Explore',
+ style: TextStyle(
+ color: home.selectedIndex == 4 ? Colors.white : Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
),
),
],
@@ -484,7 +562,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
},
child: Container(
padding: const EdgeInsets.only(left: 12),
- child: const Icon(Icons.download, color: Colors.white, size: 24),
+ child: const Icon(Icons.download, color: Colors.white, size: 28),
),
);
} else {
@@ -502,16 +580,53 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
controller: _controller!,
);
} else if (provider.selectedIndex == 2) {
- return Padding(
- padding: EdgeInsets.only(right: MediaQuery.sizeOf(context).width * 0.16),
- child: const Text('Explore', style: TextStyle(color: Colors.white, fontSize: 18)),
+ return Center(
+ child: Padding(
+ padding: EdgeInsets.only(right: MediaQuery.sizeOf(context).width * 0.10),
+ child: const Text('Memories',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.w600,
+ )),
+ ),
+ );
+ } else if (provider.selectedIndex == 3) {
+ return const Expanded(
+ child: Center(
+ child: Text(
+ 'Actions',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ );
+ } else if (provider.selectedIndex == 4) {
+ return Center(
+ child: Padding(
+ padding: EdgeInsets.only(right: MediaQuery.sizeOf(context).width * 0.10),
+ child: const Text('Explore',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.w600,
+ )),
+ ),
);
} else {
- return Expanded(
- child: Row(
- children: [
- const Spacer(),
- ],
+ return const Expanded(
+ child: Center(
+ child: Text(
+ '',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
),
);
}
@@ -519,27 +634,34 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker
),
Row(
children: [
- IconButton(
- padding: const EdgeInsets.all(8.0),
+ Container(
+ decoration: const BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.transparent,
+ ),
+ child: IconButton(
+ padding: const EdgeInsets.fromLTRB(2.0, 2.0, 0, 2.0),
icon: SvgPicture.asset(
- Assets.images.icPersonaProfile.path,
- width: 28,
- height: 28,
+ Assets.images.icSettingPersona.path,
+ width: 36,
+ height: 36,
),
onPressed: () {
- MixpanelManager().pageOpened('Persona Profile');
-
- // Set routing in provider
- var personaProvider = Provider.of(context, listen: false);
- personaProvider.setRouting(PersonaProfileRouting.home);
-
- // Navigate
- var homeProvider = Provider.of(context, listen: false);
- homeProvider.setIndex(3);
- if (homeProvider.onSelectedIndexChanged != null) {
- homeProvider.onSelectedIndexChanged!(3);
+ MixpanelManager().pageOpened('Settings');
+ String language = SharedPreferencesUtil().userPrimaryLanguage;
+ bool hasSpeech = SharedPreferencesUtil().hasSpeakerProfile;
+ String transcriptModel = SharedPreferencesUtil().transcriptionModel;
+ routeToPage(context, const SettingsPage());
+ if (language != SharedPreferencesUtil().userPrimaryLanguage ||
+ hasSpeech != SharedPreferencesUtil().hasSpeakerProfile ||
+ transcriptModel != SharedPreferencesUtil().transcriptionModel) {
+ if (context.mounted) {
+ context.read().onRecordProfileSettingChanged();
+ }
}
- }),
+ },
+ ),
+ ),
],
),
],
diff --git a/app/lib/pages/memories/memories_review_page.dart b/app/lib/pages/memories/memories_review_page.dart
index 5c1528b52..c9c5e246f 100644
--- a/app/lib/pages/memories/memories_review_page.dart
+++ b/app/lib/pages/memories/memories_review_page.dart
@@ -63,7 +63,7 @@ class _MemoriesReviewPageState extends State {
// Process memories with a small delay to allow UI to update
for (var memory in memoriesToProcess) {
await Future.delayed(const Duration(milliseconds: 20));
- context.read().reviewMemory(memory, approve);
+ context.read().reviewMemory(memory, approve, 'review_page_batch');
}
setState(() {
@@ -98,6 +98,46 @@ class _MemoriesReviewPageState extends State {
}
}
+ void _processSingleMemory(Memory memory, bool approve) async {
+ if (_isProcessing) return;
+
+ setState(() => _isProcessing = true);
+
+ // Process the single memory
+ context.read().reviewMemory(memory, approve, 'review_page_single');
+
+ setState(() {
+ remainingMemories.remove(memory);
+ displayedMemories = selectedCategory == null
+ ? List.from(remainingMemories)
+ : remainingMemories.where((f) => f.category == selectedCategory).toList();
+ _isProcessing = false;
+ });
+
+ // Show feedback
+ if (!mounted) return;
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ approve ? 'Memory saved' : 'Memory discarded',
+ style: const TextStyle(color: Colors.white),
+ ),
+ backgroundColor: Colors.grey.shade800,
+ duration: const Duration(seconds: 2),
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ ),
+ );
+
+ if (remainingMemories.isEmpty) {
+ Navigator.pop(context);
+ }
+ }
+
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, provider, child) {
@@ -110,9 +150,9 @@ class _MemoriesReviewPageState extends State {
children: [
const Text('Review Memories'),
Text(
- '${displayedMemories.length} ${selectedCategory != null ? "in ${selectedCategory.toString().split('.').last}" : "total"}',
+ _getFilterSubtitle(),
style: TextStyle(
- fontSize: 14,
+ fontSize: 13,
color: Colors.grey.shade400,
fontWeight: FontWeight.normal,
),
@@ -123,8 +163,8 @@ class _MemoriesReviewPageState extends State {
body: Column(
children: [
Container(
- height: 50,
- margin: const EdgeInsets.symmetric(vertical: 8),
+ height: 46,
+ margin: const EdgeInsets.only(top: 4, bottom: 8),
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -137,6 +177,7 @@ class _MemoriesReviewPageState extends State {
style: TextStyle(
color: selectedCategory == null ? Colors.black : Colors.white70,
fontWeight: selectedCategory == null ? FontWeight.w600 : FontWeight.normal,
+ fontSize: 13,
),
),
selected: selectedCategory == null,
@@ -145,20 +186,27 @@ class _MemoriesReviewPageState extends State {
selectedColor: Colors.white,
checkmarkColor: Colors.black,
showCheckmark: false,
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
),
...categoryCounts.entries.map((entry) {
final category = entry.key;
final count = entry.value;
+
+ // Format category name to be more concise
+ String categoryName = category.toString().split('.').last;
+ // Capitalize first letter only
+ categoryName = categoryName[0].toUpperCase() + categoryName.substring(1);
+
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(
- '${category.toString().split('.').last} ($count)',
+ '$categoryName ($count)',
style: TextStyle(
color: selectedCategory == category ? Colors.black : Colors.white70,
fontWeight: selectedCategory == category ? FontWeight.w600 : FontWeight.normal,
+ fontSize: 13,
),
),
selected: selectedCategory == category,
@@ -167,7 +215,7 @@ class _MemoriesReviewPageState extends State {
selectedColor: Colors.white,
checkmarkColor: Colors.black,
showCheckmark: false,
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
);
}),
@@ -185,10 +233,10 @@ class _MemoriesReviewPageState extends State {
Text(
selectedCategory == null
? 'All memories have been reviewed'
- : 'No memories to review in this category',
+ : 'No memories in this category',
style: TextStyle(
color: Colors.grey.shade400,
- fontSize: 18,
+ fontSize: 16,
),
),
],
@@ -200,16 +248,16 @@ class _MemoriesReviewPageState extends State {
itemBuilder: (context, index) {
final memory = displayedMemories[index];
return Container(
- margin: const EdgeInsets.only(bottom: 12),
+ margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.grey.shade900,
- borderRadius: BorderRadius.circular(16),
+ borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -217,108 +265,85 @@ class _MemoriesReviewPageState extends State {
category: memory.category,
showIcon: true,
),
- const SizedBox(height: 12),
+ const SizedBox(height: 10),
Text(
memory.content.decodeString,
style: const TextStyle(
color: Colors.white,
- fontSize: 16,
+ fontSize: 15,
height: 1.4,
),
),
],
),
),
- Row(
- children: [
- Expanded(
- child: TextButton(
- style: TextButton.styleFrom(
- backgroundColor: Colors.transparent,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- bottomLeft: Radius.circular(16),
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ borderRadius: const BorderRadius.vertical(
+ bottom: Radius.circular(12),
+ ),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
+ child: Row(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () => _processSingleMemory(memory, false),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(8),
),
- ),
- ),
- onPressed: _isProcessing
- ? null
- : () {
- provider.reviewMemory(memory, false);
- setState(() {
- remainingMemories.remove(memory);
- displayedMemories.remove(memory);
- });
- if (remainingMemories.isEmpty) {
- Navigator.pop(context);
- }
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 12),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.delete_outline, size: 18, color: Colors.red.shade400),
- const SizedBox(width: 8),
- Text(
- 'Discard',
- style: TextStyle(
- color: Colors.red.shade400,
- fontSize: 15,
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.delete_outline, size: 16, color: Colors.white70),
+ SizedBox(width: 6),
+ Text(
+ 'Discard',
+ style: TextStyle(
+ color: Colors.white70,
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
),
- ),
- Container(
- width: 1,
- height: 45,
- color: Colors.white.withOpacity(0.1),
- ),
- Expanded(
- child: TextButton(
- style: TextButton.styleFrom(
- backgroundColor: Colors.transparent,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- bottomRight: Radius.circular(16),
+ const SizedBox(width: 10),
+ Expanded(
+ child: GestureDetector(
+ onTap: () => _processSingleMemory(memory, true),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.purple.shade700,
+ borderRadius: BorderRadius.circular(8),
),
- ),
- ),
- onPressed: _isProcessing
- ? null
- : () {
- provider.reviewMemory(memory, true);
- setState(() {
- remainingMemories.remove(memory);
- displayedMemories.remove(memory);
- });
- if (remainingMemories.isEmpty) {
- Navigator.pop(context);
- }
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 12),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.check, size: 18, color: Colors.white),
- const SizedBox(width: 8),
- Text(
- 'Save',
- style: TextStyle(
- color: Colors.grey.shade100,
- fontSize: 15,
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.check, size: 16, color: Colors.white),
+ SizedBox(width: 6),
+ Text(
+ 'Save',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
),
- ),
- ],
+ ],
+ ),
),
],
),
@@ -351,56 +376,59 @@ class _MemoriesReviewPageState extends State {
: Row(
children: [
Expanded(
- child: TextButton(
- style: TextButton.styleFrom(
- backgroundColor: Colors.red.shade900.withOpacity(0.3),
+ child: GestureDetector(
+ onTap: () => _processBatchAction(false),
+ child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(10),
),
- ),
- onPressed: () => _processBatchAction(false),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.delete_outline, size: 18, color: Colors.red.shade400),
- const SizedBox(width: 8),
- Text(
- 'Discard all',
- style: TextStyle(
- color: Colors.red.shade400,
- fontSize: 15,
+ alignment: Alignment.center,
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.delete_outline, size: 18, color: Colors.white70),
+ SizedBox(width: 8),
+ Text(
+ 'Discard All',
+ style: TextStyle(
+ color: Colors.white70,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
),
const SizedBox(width: 12),
Expanded(
- child: TextButton(
- style: TextButton.styleFrom(
- backgroundColor: Colors.white,
+ child: GestureDetector(
+ onTap: () => _processBatchAction(true),
+ child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ decoration: BoxDecoration(
+ color: Colors.purple.shade700,
+ borderRadius: BorderRadius.circular(10),
),
- ),
- onPressed: () => _processBatchAction(true),
- child: const Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.check, size: 18, color: Colors.black),
- SizedBox(width: 8),
- Text(
- 'Save all',
- style: TextStyle(
- color: Colors.black,
- fontSize: 15,
- fontWeight: FontWeight.w600,
+ alignment: Alignment.center,
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.check, size: 18, color: Colors.white),
+ SizedBox(width: 8),
+ Text(
+ 'Save All',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
),
@@ -412,4 +440,15 @@ class _MemoriesReviewPageState extends State {
);
});
}
+
+ String _getFilterSubtitle() {
+ if (selectedCategory == null) {
+ return '${displayedMemories.length} memories to review';
+ }
+
+ String categoryName = selectedCategory.toString().split('.').last;
+ categoryName = categoryName[0].toUpperCase() + categoryName.substring(1);
+
+ return '${displayedMemories.length} ${categoryName} memories';
+ }
}
diff --git a/app/lib/pages/memories/page.dart b/app/lib/pages/memories/page.dart
index 6fc0c207b..c20472300 100644
--- a/app/lib/pages/memories/page.dart
+++ b/app/lib/pages/memories/page.dart
@@ -1,14 +1,22 @@
import 'package:flutter/material.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:omi/backend/schema/memory.dart';
+import 'package:omi/providers/home_provider.dart';
import 'package:omi/providers/memories_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
+import 'package:omi/utils/ui_guidelines.dart';
import 'package:omi/widgets/extensions/functions.dart';
import 'package:provider/provider.dart';
+import 'package:shimmer/shimmer.dart';
import 'widgets/memory_edit_sheet.dart';
import 'widgets/memory_item.dart';
import 'widgets/memory_dialog.dart';
import 'widgets/memory_review_sheet.dart';
+import 'widgets/memory_management_sheet.dart';
+
+// Filter options for the dropdown
+enum FilterOption { interesting, system, all }
class MemoriesPage extends StatefulWidget {
const MemoriesPage({super.key});
@@ -17,30 +25,120 @@ class MemoriesPage extends StatefulWidget {
State createState() => MemoriesPageState();
}
-class MemoriesPageState extends State {
+class _ReviewPromptHeaderDelegate extends SliverPersistentHeaderDelegate {
+ final double height;
+ final Widget child;
+
+ _ReviewPromptHeaderDelegate({
+ required this.height,
+ required this.child,
+ });
+
+ @override
+ double get minExtent => height;
+
+ @override
+ double get maxExtent => height;
+
+ @override
+ Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+ return SizedBox.expand(child: child);
+ }
+
+ @override
+ bool shouldRebuild(_ReviewPromptHeaderDelegate oldDelegate) {
+ return height != oldDelegate.height || child != oldDelegate.child;
+ }
+}
+
+class MemoriesPageState extends State with AutomaticKeepAliveClientMixin {
+ @override
+ bool get wantKeepAlive => true;
+
final TextEditingController _searchController = TextEditingController();
+ MemoryCategory? _selectedCategory;
+ final ScrollController _scrollController = ScrollController();
+
+ // Filter options for the dropdown
+ // Default will be set in initState based on current date
+ late FilterOption _currentFilter;
@override
void dispose() {
_searchController.dispose();
+ _scrollController.dispose();
super.dispose();
}
@override
void initState() {
- () async {
- await context.read().init();
+ super.initState();
+ // Set default filter based on current date
+ final now = DateTime.now();
+ final cutoffDate = DateTime(2025, 5, 31);
+
+ if (now.isAfter(cutoffDate)) {
+ _currentFilter = FilterOption.interesting;
+ } else {
+ _currentFilter = FilterOption.all;
+ }
- final unreviewedMemories = context.read().unreviewed;
- if (unreviewedMemories.isNotEmpty) {
- _showReviewSheet(unreviewedMemories);
+ (() async {
+ final provider = context.read();
+ await provider.init();
+
+ // Apply the date-based default filter
+ _applyFilter(_currentFilter);
+
+ if (!mounted) return;
+ final unreviewedMemories = provider.unreviewed;
+ final home = context.read();
+ if (unreviewedMemories.isNotEmpty && home.selectedIndex == 2) {
+ _showReviewSheet(context, unreviewedMemories, provider);
}
- }.withPostFrameCallback();
- super.initState();
+ }).withPostFrameCallback();
+ }
+
+ void _applyFilter(FilterOption option) {
+ final provider = context.read();
+ setState(() {
+ _currentFilter = option;
+
+ switch (option) {
+ case FilterOption.interesting:
+ _filterByCategory(MemoryCategory.interesting);
+ MixpanelManager().memoriesFiltered('interesting');
+ break;
+ case FilterOption.system:
+ _filterByCategory(MemoryCategory.system);
+ MixpanelManager().memoriesFiltered('system');
+ break;
+ case FilterOption.all:
+ _filterByCategory(null); // null means no category filter
+ MixpanelManager().memoriesFiltered('all');
+ break;
+ }
+ });
+ }
+
+ void _filterByCategory(MemoryCategory? category) {
+ setState(() {
+ _selectedCategory = category;
+ });
+ context.read().setCategoryFilter(category);
+ }
+
+ Map _getCategoryCounts(List memories) {
+ var counts = {};
+ for (var memory in memories) {
+ counts[memory.category] = (counts[memory.category] ?? 0) + 1;
+ }
+ return counts;
}
@override
Widget build(BuildContext context) {
+ super.build(context);
return Consumer(
builder: (context, provider, _) {
return PopScope(
@@ -48,114 +146,346 @@ class MemoriesPageState extends State {
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
body: provider.loading
- ? const Center(
- child: CircularProgressIndicator(
- valueColor: AlwaysStoppedAnimation(Colors.white),
- ))
- : CustomScrollView(
- slivers: [
- SliverAppBar(
- backgroundColor: Theme.of(context).colorScheme.primary,
- pinned: true,
- snap: true,
- floating: true,
- title: const Text('My Memory'),
- actions: [
- IconButton(
- icon: const Icon(Icons.delete_sweep_outlined),
- onPressed: () {
- _showDeleteAllConfirmation(context, provider);
- },
- ),
- IconButton(
- icon: const Icon(Icons.add),
- onPressed: () {
- showMemoryDialog(context, provider);
- MixpanelManager().memoriesPageCreateMemoryBtn();
- },
- ),
- ],
- bottom: PreferredSize(
- preferredSize: const Size.fromHeight(68),
+ ? NestedScrollView(
+ headerSliverBuilder: (context, innerBoxIsScrolled) {
+ return [
+ SliverToBoxAdapter(
child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
- child: SearchBar(
- hintText: 'Search Omi\'s memory about you',
- leading: const Icon(Icons.search, color: Colors.white70),
- backgroundColor: WidgetStateProperty.all(Colors.grey.shade900),
- elevation: WidgetStateProperty.all(0),
- padding: WidgetStateProperty.all(
- const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
- ),
- controller: _searchController,
- trailing: provider.searchQuery.isNotEmpty
- ? [
- IconButton(
- icon: const Icon(Icons.close, color: Colors.white70),
- onPressed: () {
- _searchController.clear();
- setState(() {});
- provider.setSearchQuery('');
- },
- )
- ]
- : null,
- hintStyle: WidgetStateProperty.all(
- TextStyle(color: Colors.grey.shade400, fontSize: 14),
- ),
- shape: WidgetStateProperty.all(
- RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
+ child: Row(
+ children: [
+ Expanded(
+ child: SizedBox(
+ height: 44,
+ child: SearchBar(
+ hintText: 'Search memories',
+ leading: const Padding(
+ padding: EdgeInsets.only(left: 6.0),
+ child: Icon(FontAwesomeIcons.magnifyingGlass, color: Colors.white70, size: 14),
+ ),
+ backgroundColor: WidgetStateProperty.all(AppStyles.backgroundSecondary),
+ elevation: WidgetStateProperty.all(0),
+ padding: WidgetStateProperty.all(
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
+ ),
+ hintStyle: WidgetStateProperty.all(
+ TextStyle(color: AppStyles.textTertiary, fontSize: 14),
+ ),
+ textStyle: WidgetStateProperty.all(
+ TextStyle(color: AppStyles.textPrimary, fontSize: 14),
+ ),
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(AppStyles.radiusLarge),
+ ),
+ ),
+ ),
+ ),
),
- ),
- onChanged: (value) => provider.setSearchQuery(value),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: _buildShimmerButton(),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: _buildShimmerButton(),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: _buildShimmerButton(),
+ ),
+ ],
),
),
),
- ),
- SliverPadding(
- padding: const EdgeInsets.all(16),
- sliver: provider.filteredMemories.isEmpty
- ? SliverFillRemaining(
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.note_add, size: 48, color: Colors.grey.shade600),
- const SizedBox(height: 16),
- Text(
- provider.searchQuery.isEmpty ? 'No memories yet' : 'No memories found',
- style: TextStyle(
- color: Colors.grey.shade400,
- fontSize: 18,
+ ];
+ },
+ body: _buildShimmerMemoryList(),
+ )
+ : NestedScrollView(
+ controller: _scrollController,
+ headerSliverBuilder: (context, innerBoxIsScrolled) {
+ return [
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
+ child: Row(
+ children: [
+ Consumer(builder: (context, home, child) {
+ return Expanded(
+ child: SizedBox(
+ height: 44,
+ child: SearchBar(
+ hintText: 'Search memories',
+ leading: const Padding(
+ padding: EdgeInsets.only(left: 6.0),
+ child: Icon(FontAwesomeIcons.magnifyingGlass, color: Colors.white70, size: 14),
+ ),
+ backgroundColor: WidgetStateProperty.all(AppStyles.backgroundSecondary),
+ elevation: WidgetStateProperty.all(0),
+ padding: WidgetStateProperty.all(
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
+ ),
+ focusNode: home.memoriesSearchFieldFocusNode,
+ controller: _searchController,
+ trailing: provider.searchQuery.isNotEmpty
+ ? [
+ IconButton(
+ icon: const Icon(Icons.close, color: Colors.white70, size: 16),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minHeight: 36,
+ minWidth: 36,
+ ),
+ onPressed: () {
+ _searchController.clear();
+ provider.setSearchQuery('');
+ MixpanelManager().memorySearchCleared(provider.memories.length);
+ },
+ )
+ ]
+ : null,
+ hintStyle: WidgetStateProperty.all(
+ TextStyle(color: AppStyles.textTertiary, fontSize: 14),
+ ),
+ textStyle: WidgetStateProperty.all(
+ TextStyle(color: AppStyles.textPrimary, fontSize: 14),
),
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(AppStyles.radiusLarge),
+ ),
+ ),
+ onChanged: (value) => provider.setSearchQuery(value),
+ onSubmitted: (value) {
+ if (value.isNotEmpty) {
+ MixpanelManager().memorySearched(value, provider.filteredMemories.length);
+ }
+ },
),
- if (provider.searchQuery.isEmpty) ...[
- const SizedBox(height: 8),
- TextButton(
- onPressed: () => showMemoryDialog(context, provider),
- child: const Text('Add your first memory'),
+ ),
+ );
+ }),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: PopupMenuButton(
+ onSelected: _applyFilter,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ color: AppStyles.backgroundSecondary,
+ offset: const Offset(0, 8),
+ elevation: 4,
+ tooltip: 'Filter memories by category',
+ position: PopupMenuPosition.under,
+ itemBuilder: (BuildContext context) => >[
+ PopupMenuItem(
+ value: FilterOption.all,
+ child: Row(
+ children: [
+ const Text(
+ 'All',
+ style: TextStyle(color: Colors.white),
+ ),
+ const Spacer(),
+ if (_currentFilter == FilterOption.all) const Icon(Icons.check, size: 16, color: Colors.white),
+ ],
),
- ],
+ ),
+ PopupMenuItem(
+ value: FilterOption.interesting,
+ child: Row(
+ children: [
+ const Text(
+ 'Interesting',
+ style: TextStyle(color: Colors.white),
+ ),
+ const Spacer(),
+ if (_currentFilter == FilterOption.interesting) const Icon(Icons.check, size: 16, color: Colors.white),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: FilterOption.system,
+ child: Row(
+ children: [
+ const Text(
+ 'System',
+ style: TextStyle(color: Colors.white),
+ ),
+ const Spacer(),
+ if (_currentFilter == FilterOption.system) const Icon(Icons.check, size: 16, color: Colors.white),
+ ],
+ ),
+ ),
],
+ child: Container(
+ decoration: BoxDecoration(
+ color: AppStyles.backgroundSecondary,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Center(
+ child: Icon(
+ FontAwesomeIcons.filter,
+ size: 16,
+ color: Colors.white,
+ ),
+ ),
+ ),
),
),
- )
- : SliverList(
- delegate: SliverChildBuilderDelegate(
- (context, index) {
- final memory = provider.filteredMemories[index];
- return MemoryItem(
- memory: memory,
- provider: provider,
- onTap: _showQuickEditSheet,
- );
- },
- childCount: provider.filteredMemories.length,
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: ElevatedButton(
+ onPressed: () {
+ _showMemoryManagementSheet(context, provider);
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppStyles.backgroundSecondary,
+ foregroundColor: Colors.white,
+ padding: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: const Icon(FontAwesomeIcons.sliders, size: 16),
+ ),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 44,
+ height: 44,
+ child: ElevatedButton(
+ onPressed: () {
+ showMemoryDialog(context, provider);
+ MixpanelManager().memoriesPageCreateMemoryBtn();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppStyles.backgroundSecondary,
+ foregroundColor: Colors.white,
+ padding: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: const Icon(FontAwesomeIcons.plus, size: 18),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ if (provider.unreviewed.isNotEmpty)
+ SliverPersistentHeader(
+ pinned: true,
+ floating: true,
+ delegate: _ReviewPromptHeaderDelegate(
+ height: 56.0,
+ child: Material(
+ color: Theme.of(context).colorScheme.surfaceVariant,
+ elevation: 1,
+ child: InkWell(
+ onTap: () => _showReviewSheet(context, provider.unreviewed, provider),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Row(
+ children: [
+ Icon(FontAwesomeIcons.listCheck, color: Theme.of(context).colorScheme.onSurfaceVariant, size: 18),
+ const SizedBox(width: 12),
+ Flexible(
+ child: Text(
+ '${provider.unreviewed.length} ${provider.unreviewed.length == 1 ? "memory" : "memories"} to review',
+ style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: Text('Review', style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ ),
+ ),
),
),
- ),
- ],
+ ),
+ ),
+ SliverPersistentHeader(
+ pinned: true,
+ floating: true,
+ delegate: _SliverSearchBarDelegate(
+ minHeight: 0,
+ maxHeight: 0,
+ child: Container(),
+ ),
+ ),
+ ];
+ },
+ body: provider.filteredMemories.isEmpty
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.note_add, size: 48, color: Colors.grey.shade600),
+ const SizedBox(height: 16),
+ Text(
+ provider.searchQuery.isEmpty && _selectedCategory == null
+ ? 'No memories yet'
+ : _selectedCategory != null
+ ? _selectedCategory == MemoryCategory.interesting
+ ? 'No interesting memories yet'
+ : _selectedCategory == MemoryCategory.system
+ ? 'No system memories yet'
+ : 'No memories in this category'
+ : 'No memories found',
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 18,
+ ),
+ ),
+ if (provider.searchQuery.isEmpty && _selectedCategory == null) ...[
+ const SizedBox(height: 8),
+ TextButton(
+ onPressed: () => showMemoryDialog(context, provider),
+ child: const Text('Add your first memory'),
+ ),
+ ],
+ ],
+ ),
+ )
+ : ListView.builder(
+ // Add significant bottom padding to prevent content from being covered by floating action bar
+ padding: const EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 120),
+ itemCount: provider.filteredMemories.length,
+ itemBuilder: (context, index) {
+ final memory = provider.filteredMemories[index];
+ return MemoryItem(
+ memory: memory,
+ provider: provider,
+ onTap: (BuildContext context, Memory tappedMemory, MemoriesProvider tappedProvider) {
+ MixpanelManager().memoryListItemClicked(tappedMemory);
+ _showQuickEditSheet(context, tappedMemory, tappedProvider);
+ },
+ );
+ },
+ ),
),
),
);
@@ -163,6 +493,42 @@ class MemoriesPageState extends State {
);
}
+ Widget _buildShimmerButton() {
+ return Shimmer.fromColors(
+ baseColor: AppStyles.backgroundSecondary,
+ highlightColor: AppStyles.backgroundTertiary,
+ child: Container(
+ decoration: BoxDecoration(
+ color: AppStyles.backgroundSecondary,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildShimmerMemoryList() {
+ return Padding(
+ padding: const EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 120),
+ child: ListView.builder(
+ itemCount: 8, // Show 8 shimmer items
+ itemBuilder: (context, index) {
+ return Shimmer.fromColors(
+ baseColor: AppStyles.backgroundSecondary,
+ highlightColor: AppStyles.backgroundTertiary,
+ child: Container(
+ margin: const EdgeInsets.only(bottom: AppStyles.spacingM),
+ height: 88, // Approximate height of a memory item
+ decoration: BoxDecoration(
+ color: AppStyles.backgroundSecondary,
+ borderRadius: BorderRadius.circular(AppStyles.radiusLarge),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
void _showQuickEditSheet(BuildContext context, Memory memory, MemoriesProvider provider) {
showModalBottomSheet(
context: context,
@@ -176,21 +542,23 @@ class MemoriesPageState extends State {
);
}
- void _showReviewSheet(List memories) async {
- if (memories.isEmpty) return;
+ void _showReviewSheet(BuildContext context, List memories, MemoriesProvider existingProvider) async {
+ if (memories.isEmpty || !mounted) return;
await showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isDismissible: true,
enableDrag: false,
- builder: (context) => ListenableProvider(
- create: (_) => MemoriesProvider(),
- builder: (context, _) {
- return MemoriesReviewSheet(
- memories: memories,
- provider: context.read(),
- );
- }),
+ isScrollControlled: true,
+ builder: (sheetContext) {
+ return ChangeNotifierProvider.value(
+ value: existingProvider,
+ child: MemoriesReviewSheet(
+ memories: memories,
+ provider: existingProvider,
+ ),
+ );
+ },
);
}
@@ -245,4 +613,42 @@ class MemoriesPageState extends State {
),
);
}
+
+ void _showMemoryManagementSheet(BuildContext context, MemoriesProvider provider) {
+ MixpanelManager().memoriesManagementSheetOpened();
+ showModalBottomSheet(
+ context: context,
+ backgroundColor: Colors.transparent,
+ isScrollControlled: true,
+ builder: (context) => MemoryManagementSheet(provider: provider),
+ );
+ }
+}
+
+class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
+ final double minHeight;
+ final double maxHeight;
+ final Widget child;
+
+ _SliverSearchBarDelegate({
+ required this.minHeight,
+ required this.maxHeight,
+ required this.child,
+ });
+
+ @override
+ double get minExtent => minHeight;
+
+ @override
+ double get maxExtent => maxHeight;
+
+ @override
+ Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+ return SizedBox.expand(child: child);
+ }
+
+ @override
+ bool shouldRebuild(_SliverSearchBarDelegate oldDelegate) {
+ return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child;
+ }
}
diff --git a/app/lib/pages/memories/widgets/category_chip.dart b/app/lib/pages/memories/widgets/category_chip.dart
index fff4bc54f..88c641f2c 100644
--- a/app/lib/pages/memories/widgets/category_chip.dart
+++ b/app/lib/pages/memories/widgets/category_chip.dart
@@ -19,34 +19,71 @@ class CategoryChip extends StatelessWidget {
this.showCheckmark = false,
});
+ Color _getCategoryColor() {
+ switch (category) {
+ case MemoryCategory.interesting:
+ return Colors.blue;
+ case MemoryCategory.system:
+ return Colors.grey;
+ }
+ }
+
+ IconData _getCategoryIcon() {
+ switch (category) {
+ case MemoryCategory.interesting:
+ return Icons.star_outline;
+ case MemoryCategory.system:
+ return Icons.settings_outlined;
+ }
+ }
+
@override
Widget build(BuildContext context) {
final categoryName = category.toString().split('.').last;
- final displayName = count != null ? '$categoryName ($count)' : categoryName;
+ // Use shorter display names for categories
+ String displayName;
+ switch (category) {
+ case MemoryCategory.interesting:
+ displayName = "Interesting";
+ break;
+ case MemoryCategory.system:
+ displayName = "System";
+ break;
+ }
+
+ final countText = count != null ? ' ($count)' : '';
+
+ final categoryColor = _getCategoryColor();
+ final categoryIcon = _getCategoryIcon();
Widget chip = Container(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ height: 26,
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
decoration: BoxDecoration(
- color: isSelected ? (onTap != null ? Colors.white : Colors.white.withOpacity(0.1)) : Colors.grey.shade800,
- borderRadius: BorderRadius.circular(16),
- border: isSelected && onTap == null ? Border.all(color: Colors.white, width: 1) : null,
+ color: isSelected ? (onTap != null ? categoryColor : categoryColor.withOpacity(0.15)) : Colors.grey.shade800.withOpacity(0.6),
+ borderRadius: BorderRadius.circular(13),
+ border: isSelected && onTap == null ? Border.all(color: categoryColor, width: 1) : null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (showIcon) ...[
- const Icon(Icons.label_outline, size: 14, color: Colors.white),
+ Icon(
+ categoryIcon,
+ size: 14,
+ color: isSelected && onTap != null ? Colors.white : categoryColor,
+ ),
const SizedBox(width: 4),
],
if (showCheckmark && isSelected) ...[
- const Icon(Icons.check, size: 14, color: Colors.white),
- const SizedBox(width: 4),
+ const Icon(Icons.check, size: 12, color: Colors.white),
+ const SizedBox(width: 2),
],
Text(
- displayName,
+ displayName + countText,
style: TextStyle(
- color: isSelected ? (onTap != null ? Colors.black : Colors.white) : Colors.white70,
- fontSize: 13,
+ color: isSelected ? (onTap != null ? Colors.white : categoryColor) : Colors.white70,
+ fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
@@ -61,6 +98,9 @@ class CategoryChip extends StatelessWidget {
);
}
- return chip;
+ return Align(
+ alignment: Alignment.centerLeft,
+ child: chip,
+ );
}
}
diff --git a/app/lib/pages/memories/widgets/memory_dialog.dart b/app/lib/pages/memories/widgets/memory_dialog.dart
index be811f169..f43865152 100644
--- a/app/lib/pages/memories/widgets/memory_dialog.dart
+++ b/app/lib/pages/memories/widgets/memory_dialog.dart
@@ -222,8 +222,8 @@ class _MemoryDialogState extends State {
}
MixpanelManager().memoriesPageEditedMemory();
} else {
- widget.provider.createMemory(value, selectedVisibility);
- MixpanelManager().memoriesPageCreatedMemory(MemoryCategory.values.first);
+ widget.provider.createMemory(value, selectedVisibility, MemoryCategory.interesting);
+ MixpanelManager().memoriesPageCreatedMemory(MemoryCategory.interesting);
}
Navigator.pop(context);
}
diff --git a/app/lib/pages/memories/widgets/memory_item.dart b/app/lib/pages/memories/widgets/memory_item.dart
index f5f19d61e..fe396aba6 100644
--- a/app/lib/pages/memories/widgets/memory_item.dart
+++ b/app/lib/pages/memories/widgets/memory_item.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:omi/backend/schema/memory.dart';
import 'package:omi/providers/memories_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
+import 'package:omi/utils/ui_guidelines.dart';
import 'package:omi/widgets/extensions/string.dart';
import 'delete_confirmation.dart';
@@ -25,25 +26,22 @@ class MemoryItem extends StatelessWidget {
final Widget memoryWidget = GestureDetector(
onTap: () => onTap(context, memory, provider),
child: Container(
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: Colors.grey.shade900,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ margin: const EdgeInsets.only(bottom: AppStyles.spacingM),
+ padding: const EdgeInsets.symmetric(horizontal: AppStyles.spacingL, vertical: AppStyles.spacingL),
+ decoration: AppStyles.cardDecoration,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- ListTile(
- contentPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
- title: Text(
+ Expanded(
+ child: Text(
memory.content.decodeString,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 16,
- ),
+ style: AppStyles.body,
+ maxLines: 3,
+ overflow: TextOverflow.ellipsis,
),
- trailing: _buildVisibilityButton(context),
),
+ const SizedBox(width: AppStyles.spacingM),
+ _buildVisibilityButton(context),
],
),
),
@@ -65,10 +63,10 @@ class MemoryItem extends StatelessWidget {
MixpanelManager().memoriesPageDeletedMemory(memory);
},
background: Container(
- margin: const EdgeInsets.only(bottom: 8),
+ margin: const EdgeInsets.only(bottom: AppStyles.spacingM),
decoration: BoxDecoration(
- color: Colors.red.shade900,
- borderRadius: BorderRadius.circular(12),
+ color: AppStyles.error,
+ borderRadius: BorderRadius.circular(AppStyles.radiusLarge),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
@@ -83,29 +81,31 @@ class MemoryItem extends StatelessWidget {
padding: EdgeInsets.zero,
position: PopupMenuPosition.under,
surfaceTintColor: Colors.transparent,
- color: Colors.grey.shade800,
+ color: AppStyles.backgroundTertiary,
shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ borderRadius: BorderRadius.circular(AppStyles.radiusLarge),
),
offset: const Offset(0, 4),
child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ height: 36,
+ width: 56,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
- borderRadius: BorderRadius.circular(8),
+ borderRadius: BorderRadius.circular(AppStyles.radiusMedium),
),
child: Row(
mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
memory.visibility == MemoryVisibility.private ? Icons.lock_outline : Icons.public,
size: 16,
color: Colors.white70,
),
- const SizedBox(width: 4),
+ const SizedBox(width: 6),
const Icon(
Icons.keyboard_arrow_down,
- size: 16,
+ size: 18,
color: Colors.white70,
),
],
@@ -127,6 +127,7 @@ class MemoryItem extends StatelessWidget {
],
onSelected: (visibility) {
provider.updateMemoryVisibility(memory, visibility);
+ MixpanelManager().memoryVisibilityChanged(memory, visibility);
},
);
}
@@ -162,13 +163,14 @@ class MemoryItem extends StatelessWidget {
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
- const SizedBox(height: 2),
Text(
description,
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 12,
),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
),
],
),
diff --git a/app/lib/pages/memories/widgets/memory_management_sheet.dart b/app/lib/pages/memories/widgets/memory_management_sheet.dart
new file mode 100644
index 000000000..a90c8103e
--- /dev/null
+++ b/app/lib/pages/memories/widgets/memory_management_sheet.dart
@@ -0,0 +1,280 @@
+import 'package:flutter/material.dart';
+import 'package:omi/providers/memories_provider.dart';
+import 'package:omi/utils/ui_guidelines.dart';
+
+class MemoryManagementSheet extends StatelessWidget {
+ final MemoriesProvider provider;
+
+ const MemoryManagementSheet({
+ super.key,
+ required this.provider,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: BoxDecoration(
+ color: AppStyles.backgroundSecondary,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildHeader(context),
+ const Divider(height: 1, color: Colors.white10),
+ _buildMemoryCount(context),
+ _buildActionButtons(context),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 16, 16),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Memory Management',
+ style: AppStyles.subtitle,
+ ),
+ IconButton(
+ icon: const Icon(Icons.close, color: Colors.white70),
+ onPressed: () => Navigator.pop(context),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMemoryCount(BuildContext context) {
+ final totalMemories = provider.memories.length;
+ final publicMemories = provider.memories.where((m) => !m.deleted && m.visibility.name == 'public').length;
+ final privateMemories = provider.memories.where((m) => !m.deleted && m.visibility.name == 'private').length;
+
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'You have $totalMemories total memories',
+ style: AppStyles.body,
+ ),
+ const SizedBox(height: 8),
+ _buildMemoryCountRow(Icons.public, 'Public memories', publicMemories),
+ const SizedBox(height: 4),
+ _buildMemoryCountRow(Icons.lock_outline, 'Private memories', privateMemories),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMemoryCountRow(IconData icon, String label, int count) {
+ return Row(
+ children: [
+ Icon(icon, size: 16, color: Colors.white60),
+ const SizedBox(width: 8),
+ Text(
+ label,
+ style: AppStyles.caption,
+ ),
+ const Spacer(),
+ Text(
+ count.toString(),
+ style: AppStyles.caption.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildActionButtons(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _buildActionButton(
+ context,
+ 'Make All Memories Private',
+ Icons.lock_outline,
+ Colors.white.withOpacity(0.1),
+ () => _makeAllMemoriesPrivate(context),
+ ),
+ const SizedBox(height: 12),
+ _buildActionButton(
+ context,
+ 'Make All Memories Public',
+ Icons.public,
+ Colors.white.withOpacity(0.1),
+ () => _makeAllMemoriesPublic(context),
+ ),
+ const SizedBox(height: 24),
+ const Divider(height: 1, color: Colors.white10),
+ const SizedBox(height: 24),
+ _buildActionButton(
+ context,
+ 'Delete All Memories',
+ Icons.delete_outline,
+ Colors.red.withOpacity(0.1),
+ () => _confirmDeleteAllMemories(context),
+ textColor: Colors.red,
+ iconColor: Colors.red,
+ ),
+ const SizedBox(height: 12),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildActionButton(
+ BuildContext context,
+ String text,
+ IconData icon,
+ Color backgroundColor,
+ VoidCallback onPressed, {
+ Color textColor = Colors.white,
+ Color iconColor = Colors.white,
+ }) {
+ return ElevatedButton(
+ onPressed: onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: backgroundColor,
+ foregroundColor: textColor,
+ elevation: 0,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(icon, size: 20, color: iconColor),
+ const SizedBox(width: 12),
+ Text(
+ text,
+ style: TextStyle(
+ color: textColor,
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _makeAllMemoriesPrivate(BuildContext context) async {
+ Navigator.pop(context);
+ await provider.updateAllMemoriesVisibility(true);
+
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('All memories are now private'),
+ backgroundColor: AppStyles.backgroundTertiary,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ }
+
+ void _makeAllMemoriesPublic(BuildContext context) async {
+ Navigator.pop(context);
+ await provider.updateAllMemoriesVisibility(false);
+
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('All memories are now public'),
+ backgroundColor: AppStyles.backgroundTertiary,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ }
+
+ void _confirmDeleteAllMemories(BuildContext context) {
+ if (provider.memories.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('No memories to delete'),
+ backgroundColor: AppStyles.backgroundTertiary,
+ duration: const Duration(seconds: 2),
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ ),
+ );
+ Navigator.pop(context);
+ return;
+ }
+
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: AppStyles.backgroundSecondary,
+ title: const Text(
+ 'Clear Omi\'s Memory',
+ style: TextStyle(color: Colors.white),
+ ),
+ content: Text(
+ 'Are you sure you want to clear Omi\'s memory? This action cannot be undone.',
+ style: TextStyle(color: Colors.grey.shade300),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: Text(
+ 'Cancel',
+ style: TextStyle(color: Colors.grey.shade400),
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ provider.deleteAllMemories();
+ Navigator.pop(context); // Close dialog
+ Navigator.pop(context); // Close sheet
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('Omi\'s memory about you has been cleared'),
+ backgroundColor: AppStyles.backgroundTertiary,
+ duration: const Duration(seconds: 2),
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ ),
+ );
+ },
+ child: const Text(
+ 'Clear Memory',
+ style: TextStyle(color: Colors.red),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/lib/pages/memories/widgets/memory_review_sheet.dart b/app/lib/pages/memories/widgets/memory_review_sheet.dart
index f0a343464..b91c94645 100644
--- a/app/lib/pages/memories/widgets/memory_review_sheet.dart
+++ b/app/lib/pages/memories/widgets/memory_review_sheet.dart
@@ -25,10 +25,11 @@ class MemoriesReviewSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Container(
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
- margin: const EdgeInsets.only(bottom: 16),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ margin: const EdgeInsets.only(bottom: 12),
child: Column(
children: [
+ const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -38,11 +39,11 @@ class MemoriesReviewSheet extends StatelessWidget {
SizedBox(
width: MediaQuery.of(context).size.width * 0.74,
child: Text(
- 'Review and save ${memories.length} memories generated from today\'s conversation with Omi',
+ '${memories.length} new memories to review',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
- fontWeight: FontWeight.w400,
+ fontWeight: FontWeight.w500,
),
),
),
@@ -51,28 +52,38 @@ class MemoriesReviewSheet extends StatelessWidget {
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
onPressed: () => Navigator.pop(context),
),
],
),
- const SizedBox(height: 24),
+ const SizedBox(height: 20),
Row(
children: [
Expanded(
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.1),
- padding: const EdgeInsets.symmetric(vertical: 12),
+ padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ borderRadius: BorderRadius.circular(10),
),
),
- onPressed: () => Navigator.pop(context),
+ onPressed: () {
+ Navigator.pop(context);
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => MemoriesReviewPage(memories: memories),
+ ),
+ );
+ },
child: Text(
- 'Review later',
+ 'Review Manually',
style: TextStyle(
color: Colors.grey.shade300,
- fontSize: 15,
+ fontSize: 14,
),
),
),
@@ -82,25 +93,22 @@ class MemoriesReviewSheet extends StatelessWidget {
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.white,
- padding: const EdgeInsets.symmetric(vertical: 12),
+ padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
+ borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
+ for (var memory in memories) {
+ provider.reviewMemory(memory, true, 'review_sheet_accept_all');
+ }
Navigator.pop(context);
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => MemoriesReviewPage(memories: memories),
- ),
- );
},
child: const Text(
- 'Review now',
+ 'Accept All',
style: TextStyle(
color: Colors.black,
- fontSize: 15,
+ fontSize: 14,
fontWeight: FontWeight.w600,
),
),
@@ -108,6 +116,7 @@ class MemoriesReviewSheet extends StatelessWidget {
),
],
),
+ const SizedBox(height: 10),
],
),
),
diff --git a/app/lib/pages/onboarding/device_selection.dart b/app/lib/pages/onboarding/device_selection.dart
index ce584a35e..d6645b3d4 100644
--- a/app/lib/pages/onboarding/device_selection.dart
+++ b/app/lib/pages/onboarding/device_selection.dart
@@ -109,22 +109,22 @@ class _DeviceSelectionPageState extends State with SingleTi
const SizedBox(
height: 24,
),
- TextButton(
- onPressed: () async {
- Navigator.pushReplacement(
- context,
- MaterialPageRoute(builder: (context) => const SocialHandleScreen()),
- );
- },
- child: const Text(
- 'I don\'t have omi',
- style: TextStyle(
- color: Colors.white,
- fontWeight: FontWeight.bold,
- fontSize: 16,
- ),
- ),
- ),
+ // TextButton(
+ // onPressed: () async {
+ // Navigator.pushReplacement(
+ // context,
+ // MaterialPageRoute(builder: (context) => const SocialHandleScreen()),
+ // );
+ // },
+ // child: const Text(
+ // 'I don\'t have omi',
+ // style: TextStyle(
+ // color: Colors.white,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 16,
+ // ),
+ // ),
+ // ),
],
),
const Spacer(flex: 3),
diff --git a/app/lib/pages/onboarding/primary_language/primary_language_widget.dart b/app/lib/pages/onboarding/primary_language/primary_language_widget.dart
index 5935aea8f..96891514a 100644
--- a/app/lib/pages/onboarding/primary_language/primary_language_widget.dart
+++ b/app/lib/pages/onboarding/primary_language/primary_language_widget.dart
@@ -58,8 +58,7 @@ class _LanguageSelectorWidgetState extends State {
filteredLanguages = List.from(languages);
} else {
filteredLanguages = languages.where((lang) {
- return lang.key.toLowerCase().contains(searchQuery) ||
- lang.value.toLowerCase().contains(searchQuery);
+ return lang.key.toLowerCase().contains(searchQuery) || lang.value.toLowerCase().contains(searchQuery);
}).toList();
}
@@ -81,17 +80,19 @@ class _LanguageSelectorWidgetState extends State {
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ textBaseline: TextBaseline.alphabetic,
children: [
- IconButton(
- icon: const Icon(Icons.close, color: Colors.white),
- onPressed: () => Navigator.pop(context),
- ),
- const Text(
- 'Select your primary language',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: Colors.white,
+ const Flexible(
+ child: Text(
+ 'Select your primary language',
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ softWrap: true,
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
),
),
TextButton(
@@ -107,7 +108,8 @@ class _LanguageSelectorWidgetState extends State {
child: Text(
'Done',
style: TextStyle(
- color: currentSelectedLanguage == null ? null : Colors.white,
+ color:
+ currentSelectedLanguage == null ? null : Colors.white,
),
),
),
@@ -213,8 +215,7 @@ class _PrimaryLanguageWidgetState extends State {
// Find the language name for the saved language code
final homeProvider = Provider.of(context, listen: false);
try {
- selectedLanguageName =
- homeProvider.availableLanguages.entries.firstWhere((entry) => entry.value == savedLanguage).key;
+ selectedLanguageName = homeProvider.availableLanguages.entries.firstWhere((entry) => entry.value == savedLanguage).key;
} catch (e) {
// If language not found in the map, just use the code
selectedLanguageName = savedLanguage;
@@ -305,12 +306,7 @@ class _PrimaryLanguageWidgetState extends State {
width: double.infinity,
decoration: BoxDecoration(
border: const GradientBoxBorder(
- gradient: LinearGradient(colors: [
- Color.fromARGB(127, 208, 208, 208),
- Color.fromARGB(127, 188, 99, 121),
- Color.fromARGB(127, 86, 101, 182),
- Color.fromARGB(127, 126, 190, 236)
- ]),
+ gradient: LinearGradient(colors: [Color.fromARGB(127, 208, 208, 208), Color.fromARGB(127, 188, 99, 121), Color.fromARGB(127, 86, 101, 182), Color.fromARGB(127, 126, 190, 236)]),
width: 2,
),
borderRadius: BorderRadius.circular(12),
diff --git a/app/lib/pages/persona/persona_profile.dart b/app/lib/pages/persona/persona_profile.dart
index 801b3a508..bcaddeb3c 100644
--- a/app/lib/pages/persona/persona_profile.dart
+++ b/app/lib/pages/persona/persona_profile.dart
@@ -6,16 +6,11 @@ import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/app.dart';
import 'package:omi/gen/assets.gen.dart';
import 'package:omi/main.dart';
-import 'package:omi/pages/chat/clone_chat_page.dart';
import 'package:omi/pages/onboarding/wrapper.dart';
import 'package:omi/pages/persona/persona_provider.dart';
-import 'package:omi/providers/app_provider.dart';
import 'package:omi/providers/auth_provider.dart';
import 'package:omi/providers/home_provider.dart';
import 'package:omi/pages/persona/twitter/social_profile.dart';
-import 'package:omi/pages/settings/page.dart';
-import 'package:omi/providers/capture_provider.dart';
-import 'package:omi/providers/message_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
import 'package:posthog_flutter/posthog_flutter.dart';
@@ -46,10 +41,20 @@ class _PersonaProfilePageState extends State {
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final provider = Provider.of(context, listen: false);
+ String source = 'unknown'; // Default source
+
+ // Check if we came from settings
+ final isFromSettings = ModalRoute.of(context)?.settings.arguments == 'from_settings';
+ if (isFromSettings) {
+ provider.setRouting(PersonaProfileRouting.home);
+ }
+
if (provider.routing == PersonaProfileRouting.apps_updates && provider.userPersona != null) {
provider.prepareUpdatePersona(provider.userPersona!);
+ MixpanelManager().personaProfileViewed(personaId: provider.userPersona!.id, source: source);
} else {
await provider.getVerifiedUserPersona();
+ MixpanelManager().personaProfileViewed(personaId: provider.userPersona?.id, source: source);
}
});
super.initState();
@@ -71,95 +76,37 @@ class _PersonaProfilePageState extends State {
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
- leading: Consumer(builder: (context, personaProvider, _) {
- return personaProvider.routing == PersonaProfileRouting.apps_updates
- ? IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.white),
- onPressed: () {
- Navigator.pop(context);
- },
- )
- : GestureDetector(
- onTap: () async {
- if (personaProvider.routing == PersonaProfileRouting.no_device) {
- routeToPage(context, const CloneChatPage(), replace: false);
- } else {
- context.read().setIndex(1);
- if (context.read().onSelectedIndexChanged != null) {
- context.read().onSelectedIndexChanged!(1);
- }
- var appId = persona!.id;
- var appProvider = Provider.of(context, listen: false);
- var messageProvider = Provider.of(context, listen: false);
- App? selectedApp;
- if (appId.isNotEmpty) {
- selectedApp = await appProvider.getAppFromId(appId);
- }
- appProvider.setSelectedChatAppId(appId);
- await messageProvider.refreshMessages();
- if (messageProvider.messages.isEmpty) {
- messageProvider.sendInitialAppMessage(selectedApp);
- }
- }
- },
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: SvgPicture.asset(
- Assets.images.icCloneChat.path,
- width: 24,
- height: 24,
- ),
- ),
- );
- }),
- actions: [
- // Only show settings icon for create_my_clone or home routing
- Consumer(builder: (context, personaProvider, _) {
- if (personaProvider.routing == PersonaProfileRouting.no_device) {
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: GestureDetector(
- onTap: () async {
- await routeToPage(context, const SettingsPage(mode: SettingsMode.no_device));
- },
- child: SvgPicture.asset(
- Assets.images.icSettingPersona.path,
- width: 44,
- height: 44,
- ),
- ),
- );
- }
- if (personaProvider.routing == PersonaProfileRouting.create_my_clone ||
- personaProvider.routing == PersonaProfileRouting.home) {
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: GestureDetector(
- onTap: () async {
- MixpanelManager().pageOpened('Settings');
- String language = SharedPreferencesUtil().userPrimaryLanguage;
- bool hasSpeech = SharedPreferencesUtil().hasSpeakerProfile;
- String transcriptModel = SharedPreferencesUtil().transcriptionModel;
- await routeToPage(context, const SettingsPage());
- if (language != SharedPreferencesUtil().userPrimaryLanguage ||
- hasSpeech != SharedPreferencesUtil().hasSpeakerProfile ||
- transcriptModel != SharedPreferencesUtil().transcriptionModel) {
- if (context.mounted) {
- context.read().onRecordProfileSettingChanged();
- }
- }
- },
- child: SvgPicture.asset(
- Assets.images.icSettingPersona.path,
- width: 44,
- height: 44,
- ),
- ),
- );
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Colors.white),
+ onPressed: () {
+ final isFromSettings = ModalRoute.of(context)?.settings.arguments == 'from_settings';
+
+ if (isFromSettings) {
+ // If we came from settings, just pop back to it
+ Navigator.pop(context);
+ } else if (provider.routing == PersonaProfileRouting.apps_updates) {
+ // Regular back for apps updates flow
+ Navigator.pop(context);
+ } else {
+ // For main app navigation
+ var homeProvider = Provider.of(context, listen: false);
+ homeProvider.setIndex(0); // Go back to home tab
+ if (homeProvider.onSelectedIndexChanged != null) {
+ homeProvider.onSelectedIndexChanged!(0);
+ }
}
- return const SizedBox.shrink();
- }),
- ],
+ },
+ ),
+ title: const Text(
+ 'Persona',
+ style: TextStyle(
+ fontSize: 22,
+ fontWeight: FontWeight.w600,
+ color: Colors.white,
+ ),
+ ),
+ centerTitle: true,
+ actions: [], // Empty actions - no settings button needed
),
body: persona == null
? const Center(
@@ -320,6 +267,8 @@ class _PersonaProfilePageState extends State {
await Posthog().capture(eventName: 'share_persona_clicked', properties: {
'persona_username': persona.username ?? '',
});
+ MixpanelManager()
+ .personaShared(personaId: persona.id, personaUsername: persona.username);
Share.share(
'https://personas.omi.me/u/${persona.username}',
subject: '${persona.getName()} Persona',
@@ -738,6 +687,8 @@ class _PersonaProfilePageState extends State {
bool isComingSoon = false,
bool showConnect = false,
}) {
+ final Color grayedOutColor = Colors.grey[600]!;
+
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
@@ -759,7 +710,7 @@ class _PersonaProfilePageState extends State {
Text(
text,
style: TextStyle(
- color: isComingSoon ? Colors.grey[600] : Colors.white,
+ color: isComingSoon ? grayedOutColor : Colors.white,
fontSize: 16,
),
),
@@ -771,10 +722,10 @@ class _PersonaProfilePageState extends State {
color: const Color(0xFF373737),
borderRadius: BorderRadius.circular(16),
),
- child: const Text(
+ child: Text(
'Coming soon',
style: TextStyle(
- color: Colors.white,
+ color: grayedOutColor,
fontSize: 12,
),
),
diff --git a/app/lib/pages/persona/persona_provider.dart b/app/lib/pages/persona/persona_provider.dart
index b2d74b613..d03d1f00e 100644
--- a/app/lib/pages/persona/persona_provider.dart
+++ b/app/lib/pages/persona/persona_provider.dart
@@ -6,6 +6,7 @@ import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/app.dart';
import 'package:omi/utils/alerts/app_snackbar.dart';
import 'package:image_picker/image_picker.dart';
+import 'package:omi/utils/analytics/mixpanel.dart';
typedef ShowSuccessDialogCallback = void Function(String url);
@@ -77,12 +78,17 @@ class PersonaProvider extends ChangeNotifier {
if (res['status'] == 'notfound') {
AppSnackbar.showSnackbarError('Twitter handle not found');
_twitterProfile = {};
+ MixpanelManager().personaTwitterProfileFetched(twitterHandle: handle, fetchSuccessful: false);
} else if (res['status'] == 'suspended') {
AppSnackbar.showSnackbarError('Twitter handle is suspended');
_twitterProfile = {};
+ MixpanelManager().personaTwitterProfileFetched(twitterHandle: handle, fetchSuccessful: false);
} else {
_twitterProfile = res;
+ MixpanelManager().personaTwitterProfileFetched(twitterHandle: handle, fetchSuccessful: true);
}
+ } else {
+ MixpanelManager().personaTwitterProfileFetched(twitterHandle: handle, fetchSuccessful: false);
}
setIsLoading(false);
notifyListeners();
@@ -93,6 +99,10 @@ class PersonaProvider extends ChangeNotifier {
if (!verified) {
AppSnackbar.showSnackbarError('Failed to verify Twitter handle');
}
+ MixpanelManager().personaTwitterOwnershipVerified(
+ personaId: verifiedPersonaId ?? personaId,
+ twitterHandle: _twitterProfile['profile'],
+ verificationSuccessful: verified);
SharedPreferencesUtil().hasPersonaCreated = true;
SharedPreferencesUtil().verifiedPersonaId = verifiedPersonaId;
toggleTwitterConnection(true);
@@ -158,6 +168,9 @@ class PersonaProvider extends ChangeNotifier {
// Update
debugPrint("setPersonaPublic");
+ if (_userPersona != null) {
+ MixpanelManager().personaPublicToggled(personaId: _userPersona!.id, isPublic: makePersonaPublic);
+ }
updatePersona();
}
@@ -180,6 +193,7 @@ class PersonaProvider extends ChangeNotifier {
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
selectedImage = File(image.path);
+ MixpanelManager().personaCreateImagePicked();
validateForm();
}
notifyListeners();
@@ -190,6 +204,11 @@ class PersonaProvider extends ChangeNotifier {
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
selectedImage = File(image.path);
+ if (_userPersona != null) {
+ MixpanelManager().personaUpdateImagePicked(personaId: _userPersona!.id);
+ } else {
+ MixpanelManager().personaCreateImagePicked();
+ }
validateForm();
// Update
@@ -224,6 +243,9 @@ class PersonaProvider extends ChangeNotifier {
void toggleOmiConnection(bool value) {
hasOmiConnection = value;
+ if (_userPersona != null) {
+ MixpanelManager().personaOmiConnectionToggled(personaId: _userPersona!.id, omiConnected: value);
+ }
notifyListeners();
}
@@ -232,6 +254,9 @@ class PersonaProvider extends ChangeNotifier {
if (!value) {
_twitterProfile = {};
}
+ if (_userPersona != null) {
+ MixpanelManager().personaTwitterConnectionToggled(personaId: _userPersona!.id, twitterConnected: value);
+ }
notifyListeners();
}
@@ -242,6 +267,9 @@ class PersonaProvider extends ChangeNotifier {
debugPrint("disconnectTwitter");
if (_isEditablePersona()) {
updatePersona();
+ if (_userPersona != null) {
+ MixpanelManager().personaTwitterConnectionToggled(personaId: _userPersona!.id, twitterConnected: false);
+ }
}
notifyListeners();
}
@@ -251,6 +279,9 @@ class PersonaProvider extends ChangeNotifier {
debugPrint("disconnectOmi");
if (_isEditablePersona()) {
updatePersona();
+ if (_userPersona != null) {
+ MixpanelManager().personaOmiConnectionToggled(personaId: _userPersona!.id, omiConnected: false);
+ }
}
notifyListeners();
}
@@ -265,6 +296,7 @@ class PersonaProvider extends ChangeNotifier {
return;
}
+ MixpanelManager().personaUpdateStarted(personaId: _userPersona!.id);
setIsLoading(true);
try {
Map personaData = {
@@ -297,23 +329,42 @@ class PersonaProvider extends ChangeNotifier {
_userPersona!.connectedAccounts.where((element) => element != 'twitter').toList();
}
+ List updatedFields = [];
+ if (personaData['name'] != _userPersona!.name) updatedFields.add('name');
+ if (personaData['username'] != _userPersona!.username) updatedFields.add('username');
+ if (personaData['private'] == _userPersona!.private) {
+ updatedFields.add('privacy');
+ }
+ if (selectedImage != null) updatedFields.add('image');
+
bool success = await updatePersonaApp(selectedImage, personaData);
if (success) {
AppSnackbar.showSnackbarSuccess('Persona updated successfully');
+ MixpanelManager().personaUpdated(
+ personaId: _userPersona!.id,
+ isPublic: !(personaData['private'] as bool? ?? true),
+ updatedFields: updatedFields,
+ connectedAccounts: personaData['connected_accounts'] as List?,
+ hasOmiConnection: (personaData['connected_accounts'] as List?)?.contains('omi'),
+ hasTwitterConnection: (personaData['connected_accounts'] as List?)?.contains('twitter'));
await getVerifiedUserPersona();
notifyListeners();
} else {
AppSnackbar.showSnackbarError('Failed to update persona');
+ MixpanelManager()
+ .personaUpdateFailed(personaId: _userPersona!.id, errorMessage: 'Failed to update persona API call');
}
} catch (e) {
print('Error updating persona: $e');
AppSnackbar.showSnackbarError('Failed to update persona');
+ MixpanelManager().personaUpdateFailed(personaId: _userPersona!.id, errorMessage: e.toString());
} finally {
setIsLoading(false);
}
}
Future createPersona() async {
+ MixpanelManager().personaCreateStarted();
if (!formKey.currentState!.validate() || selectedImage == null) {
if (selectedImage == null) {
AppSnackbar.showSnackbarError('Please select an image');
@@ -353,14 +404,23 @@ class PersonaProvider extends ChangeNotifier {
if (res.isNotEmpty) {
String personaUrl = 'personas.omi.me/u/${res['username']}';
debugPrint('Persona URL: $personaUrl');
+ MixpanelManager().personaCreated(
+ personaId: res['id'],
+ isPublic: !(personaData['private'] as bool? ?? true),
+ connectedAccounts: personaData['connected_accounts'] as List?,
+ hasOmiConnection: (personaData['connected_accounts'] as List?)?.contains('omi'),
+ hasTwitterConnection: (personaData['connected_accounts'] as List?)?.contains('twitter'));
if (onShowSuccessDialog != null) {
onShowSuccessDialog!(personaUrl);
}
} else {
AppSnackbar.showSnackbarError('Failed to create your persona. Please try again later.');
+ MixpanelManager().personaCreateFailed(errorMessage: 'API response empty or no ID');
}
} catch (e) {
AppSnackbar.showSnackbarError('Failed to create persona: $e');
+ MixpanelManager().personaCreateFailed(errorMessage: e.toString());
+ setIsLoading(false);
} finally {
setIsLoading(false);
}
@@ -369,6 +429,7 @@ class PersonaProvider extends ChangeNotifier {
Future checkIsUsernameTaken(String username) async {
setIsCheckingUsername(true);
isUsernameTaken = await checkPersonaUsername(username);
+ MixpanelManager().personaUsernameCheck(username: username, isTaken: isUsernameTaken);
setIsCheckingUsername(false);
}
@@ -390,13 +451,18 @@ class PersonaProvider extends ChangeNotifier {
try {
var enabled = await enableAppServer(_userPersona!.id);
if (enabled) {
+ MixpanelManager().personaEnabled(personaId: _userPersona!.id);
return true;
} else {
AppSnackbar.showSnackbarError('Failed to enable persona');
+ MixpanelManager().personaEnableFailed(personaId: _userPersona!.id, errorMessage: 'API returned false');
return false;
}
} catch (e) {
AppSnackbar.showSnackbarError('Error enabling persona: $e');
+ if (_userPersona != null) {
+ MixpanelManager().personaEnableFailed(personaId: _userPersona!.id, errorMessage: e.toString());
+ }
return false;
} finally {
setIsLoading(false);
diff --git a/app/lib/pages/settings/about.dart b/app/lib/pages/settings/about.dart
index a1bc7ba4b..53b6a4d16 100644
--- a/app/lib/pages/settings/about.dart
+++ b/app/lib/pages/settings/about.dart
@@ -60,7 +60,7 @@ class _AboutOmiPageState extends State {
ListTile(
contentPadding: const EdgeInsets.fromLTRB(4, 0, 24, 0),
title: const Text('Join the community!', style: TextStyle(color: Colors.white)),
- subtitle: const Text('4900+ members and counting.'),
+ subtitle: const Text('7000+ members and counting.'),
trailing: const Icon(Icons.discord, color: Colors.purple, size: 20),
onTap: () {
MixpanelManager().pageOpened('About Join Discord');
diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart
index 8d50db0b5..e3fde2a00 100644
--- a/app/lib/pages/settings/page.dart
+++ b/app/lib/pages/settings/page.dart
@@ -56,23 +56,19 @@ class _SettingsPageState extends State {
bool loadingExportMemories = false;
Widget _buildOmiModeContent(BuildContext context) {
+ // Group settings by category: Account, Device, Support, Info, Actions
return Column(
children: [
- const SizedBox(height: 32.0),
- getItemAddOn2(
- 'Need Help? Chat with us',
- () async {
- await Intercom.instance.displayMessenger();
- },
- icon: Icons.chat,
- ),
- const SizedBox(height: 20),
+ const SizedBox(height: 24.0),
+ // Account Settings
getItemAddOn2(
'Profile',
() => routeToPage(context, const ProfilePage()),
- icon: Icons.person,
+ icon: const Icon(Icons.person, color: Colors.white, size: 22),
),
- const SizedBox(height: 20),
+ const SizedBox(height: 12),
+
+ // Device Settings
getItemAddOn2(
'Device Settings',
() {
@@ -82,56 +78,82 @@ class _SettingsPageState extends State {
),
);
},
- icon: Icons.bluetooth_connected_sharp,
+ icon: const Icon(Icons.bluetooth_connected_sharp, color: Colors.white, size: 22),
+ ),
+ const SizedBox(height: 12),
+
+ // Advanced Settings
+ getItemAddOn2(
+ 'Developer Mode',
+ () async {
+ await routeToPage(context, const DeveloperSettingsPage());
+ setState(() {});
+ },
+ icon: const Icon(Icons.code, color: Colors.white, size: 22),
),
- const SizedBox(height: 8),
+ const SizedBox(height: 12),
+
+ // Help & Support
getItemAddOn2(
'Guides & Tutorials',
() async {
await Intercom.instance.displayHelpCenter();
},
- icon: Icons.help_outline_outlined,
+ icon: const Icon(Icons.help_outline_outlined, color: Colors.white, size: 22),
),
- const SizedBox(height: 20),
+ const SizedBox(height: 12),
+ getItemAddOn2(
+ 'Need Help? Chat with us',
+ () async {
+ await Intercom.instance.displayMessenger();
+ },
+ icon: const Icon(Icons.chat, color: Colors.white, size: 22),
+ ),
+ const SizedBox(height: 12),
+
+ // Information
getItemAddOn2(
'About Omi',
() => routeToPage(context, const AboutOmiPage()),
- icon: Icons.workspace_premium_sharp,
+ icon: const Icon(Icons.info_outline, color: Colors.white, size: 22),
),
- const SizedBox(height: 8),
- getItemAddOn2('Developer Mode', () async {
- await routeToPage(context, const DeveloperSettingsPage());
- setState(() {});
- }, icon: Icons.code),
- const SizedBox(height: 32),
- getItemAddOn2('Sign Out', () async {
- await showDialog(
- context: context,
- builder: (ctx) {
- return getDialog(context, () {
- Navigator.of(context).pop();
- }, () async {
- await SharedPreferencesUtil().clearUserPreferences();
- Provider.of(context, listen: false).setRouting(PersonaProfileRouting.no_device);
- await signOut();
- Navigator.of(context).pop();
- routeToPage(context, const DeciderWidget(), replace: true);
- }, "Sign Out?", "Are you sure you want to sign out?");
- },
- );
- }, icon: Icons.logout),
const SizedBox(height: 24),
+
+ // Actions
+ getItemAddOn2(
+ 'Sign Out',
+ () async {
+ await showDialog(
+ context: context,
+ builder: (ctx) {
+ return getDialog(context, () {
+ Navigator.of(context).pop();
+ }, () async {
+ await SharedPreferencesUtil().clearUserPreferences();
+ Provider.of