Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/lib/backend/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,21 @@ class SharedPreferencesUtil {

bool get hasViewedWrapped2025 => getBool('hasViewedWrapped2025', defaultValue: false);

// Home onboarding - track completion
bool get hasOpenedBrainMap => getBool('hasOpenedBrainMap', defaultValue: false);

set hasOpenedBrainMap(bool value) => saveBool('hasOpenedBrainMap', value);

int get homeOnboardingViewCount => getInt('homeOnboardingViewCount', defaultValue: 0);

set homeOnboardingViewCount(int value) => saveInt('homeOnboardingViewCount', value);

void incrementHomeOnboardingViewCount() {
homeOnboardingViewCount = homeOnboardingViewCount + 1;
}

bool get isHomeOnboardingCompleted => hasOpenedBrainMap || homeOnboardingViewCount >= 10;
Comment on lines +190 to +203
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The value 10 used as the view count threshold is a magic number. It should be extracted into a named constant to improve readability and maintainability.

Suggested change
// Home onboarding - track completion
bool get hasOpenedBrainMap => getBool('hasOpenedBrainMap', defaultValue: false);
set hasOpenedBrainMap(bool value) => saveBool('hasOpenedBrainMap', value);
int get homeOnboardingViewCount => getInt('homeOnboardingViewCount', defaultValue: 0);
set homeOnboardingViewCount(int value) => saveInt('homeOnboardingViewCount', value);
void incrementHomeOnboardingViewCount() {
homeOnboardingViewCount = homeOnboardingViewCount + 1;
}
bool get isHomeOnboardingCompleted => hasOpenedBrainMap || homeOnboardingViewCount >= 10;
// Home onboarding - track completion
static const int homeOnboardingViewCountThreshold = 10;
bool get hasOpenedBrainMap => getBool('hasOpenedBrainMap', defaultValue: false);
set hasOpenedBrainMap(bool value) => saveBool('hasOpenedBrainMap', value);
int get homeOnboardingViewCount => getInt('homeOnboardingViewCount', defaultValue: 0);
set homeOnboardingViewCount(int value) => saveInt('homeOnboardingViewCount', value);
void incrementHomeOnboardingViewCount() {
homeOnboardingViewCount = homeOnboardingViewCount + 1;
}
bool get isHomeOnboardingCompleted => hasOpenedBrainMap || homeOnboardingViewCount >= homeOnboardingViewCountThreshold;
References
  1. Ensure consistency in constant values used for calculations across different parts of the codebase, especially when those calculations affect user-facing metrics like progress reporting. Define a single, well-defined constant and reuse it to avoid inaccuracies.


set conversationEventsToggled(bool value) => saveBool('conversationEventsToggled', value);

bool get conversationEventsToggled => getBool('conversationEventsToggled');
Expand Down
30 changes: 28 additions & 2 deletions app/lib/pages/action_items/action_items_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'widgets/action_item_form_sheet.dart';
import 'widgets/tasks_onboarding_widget.dart';

import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/schema.dart';
import 'package:omi/providers/action_items_provider.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
Expand Down Expand Up @@ -38,16 +40,36 @@ class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAli
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
MixpanelManager().actionItemsPageOpened();
final provider = Provider.of<ActionItemsProvider>(context, listen: false);
if (provider.actionItems.isEmpty) {
provider.fetchActionItems(showShimmer: true);
await provider.fetchActionItems(showShimmer: true);
}

// Create onboarding task if needed
if (!SharedPreferencesUtil().isHomeOnboardingCompleted) {
_createOnboardingTaskIfNeeded(provider);
}
});
}

void _createOnboardingTaskIfNeeded(ActionItemsProvider provider) {
// Check if "Open your brain map" task already exists
const onboardingTaskDescription = 'Open your brain map';
final existingTask = provider.actionItems.any(
(item) => item.description.toLowerCase() == onboardingTaskDescription.toLowerCase(),
);

if (!existingTask) {
provider.createActionItem(
description: onboardingTaskDescription,
dueAt: null, // No deadline
);
}
}

@override
void dispose() {
_scrollController.removeListener(_onScroll);
Expand Down Expand Up @@ -373,6 +395,10 @@ class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAli
),
),

// Tasks onboarding widget
if (!SharedPreferencesUtil().isHomeOnboardingCompleted)
const SliverToBoxAdapter(child: TasksOnboardingWidget()),
Comment on lines +399 to +400
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The visibility of TasksOnboardingWidget is controlled by this if condition. However, the widget itself contains a similar check, making this one redundant. To improve encapsulation and reduce code duplication, it's better to let the widget manage its own visibility and remove the check here. This principle also applies to HomeOnboardingWidget in conversations_page.dart and MemoriesOnboardingWidget in memories/page.dart.

Suggested change
if (!SharedPreferencesUtil().isHomeOnboardingCompleted)
const SliverToBoxAdapter(child: TasksOnboardingWidget()),
const SliverToBoxAdapter(child: TasksOnboardingWidget()),


// Bottom padding
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
],
Expand Down
94 changes: 94 additions & 0 deletions app/lib/pages/action_items/widgets/tasks_onboarding_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:omi/backend/preferences.dart';

class TasksOnboardingWidget extends StatelessWidget {
const TasksOnboardingWidget({super.key});

@override
Widget build(BuildContext context) {
if (SharedPreferencesUtil().isHomeOnboardingCompleted) {
return const SizedBox.shrink();
}

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Your tasks are created\nautomatically\nfrom conversations and chat",
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.3,
),
),
const SizedBox(height: 12),
// Vertical line
Padding(
padding: const EdgeInsets.only(left: 40),
child: Container(
width: 2,
height: 40,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
Text(
"Now let's check your memories",
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 8),
// Arrow pointing down to memories tab (3rd icon, right of center)
Padding(
padding: const EdgeInsets.only(left: 220),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using a hardcoded padding value left: 220 is not responsive and will cause UI issues on different screen sizes. The arrow will not be positioned correctly under the 'Memories' tab. Please use a responsive approach, for example by using MediaQuery.of(context).size.width to calculate the padding dynamically.

child: CustomPaint(
size: const Size(40, 70),
painter: _ArrowDownPainter(),
),
),
],
),
);
}
}

// Arrow pointing straight down
class _ArrowDownPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey.shade600
..strokeWidth = 2
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;

// Curved line going down
final path = Path();
path.moveTo(size.width * 0.5, 0);
path.quadraticBezierTo(
size.width * 0.6,
size.height * 0.5,
size.width * 0.5,
size.height * 0.85,
);

canvas.drawPath(path, paint);

// Arrow head pointing down
final arrowPath = Path();
arrowPath.moveTo(size.width * 0.5 - 6, size.height * 0.72);
arrowPath.lineTo(size.width * 0.5, size.height * 0.85);
arrowPath.lineTo(size.width * 0.5 + 6, size.height * 0.72);

canvas.drawPath(arrowPath, paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Comment on lines +62 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _ArrowDownPainter class is duplicated in app/lib/pages/conversations/widgets/home_onboarding_widget.dart. To follow the DRY (Don't Repeat Yourself) principle and improve maintainability, this class should be extracted into a shared file (e.g., in a widgets or utils directory) and imported where needed. The leading underscore in the class name should also be removed to make it public.

13 changes: 12 additions & 1 deletion app/lib/pages/conversations/conversations_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/conversation.dart';
import 'package:omi/pages/capture/widgets/widgets.dart';
import 'package:omi/pages/conversations/widgets/processing_capture.dart';
Expand All @@ -22,6 +23,7 @@ import 'package:visibility_detector/visibility_detector.dart';
import 'widgets/empty_conversations.dart';
import 'widgets/conversations_group_widget.dart';
import 'widgets/goal_tracker_widget.dart';
import 'widgets/home_onboarding_widget.dart';

class ConversationsPage extends StatefulWidget {
const ConversationsPage({super.key});
Expand Down Expand Up @@ -63,6 +65,11 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
if (mounted && conversationProvider.conversations.isNotEmpty) {
await _appReviewService.showReviewPromptIfNeeded(context, isProcessingFirstConversation: true);
}

// Increment onboarding view count if not completed
if (!SharedPreferencesUtil().isHomeOnboardingCompleted) {
SharedPreferencesUtil().incrementHomeOnboardingViewCount();
}
});
}

Expand Down Expand Up @@ -229,7 +236,8 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
const DailySummariesList()
else if (convoProvider.groupedConversations.isEmpty &&
!convoProvider.isLoadingConversations &&
!convoProvider.isFetchingConversations)
!convoProvider.isFetchingConversations &&
SharedPreferencesUtil().isHomeOnboardingCompleted)
SliverToBoxAdapter(
child: Center(
child: Padding(
Expand Down Expand Up @@ -290,6 +298,9 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
},
),
),
// Home onboarding widget - shown AFTER conversations
if (!SharedPreferencesUtil().isHomeOnboardingCompleted)
const SliverToBoxAdapter(child: HomeOnboardingWidget()),
SliverToBoxAdapter(
child: SizedBox(height: convoProvider.isSelectionModeActive ? 160 : 100),
),
Expand Down
94 changes: 94 additions & 0 deletions app/lib/pages/conversations/widgets/home_onboarding_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:omi/backend/preferences.dart';

class HomeOnboardingWidget extends StatelessWidget {
const HomeOnboardingWidget({super.key});

@override
Widget build(BuildContext context) {
if (SharedPreferencesUtil().isHomeOnboardingCompleted) {
return const SizedBox.shrink();
}

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Here you'll see your\nconversations",
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 18,
fontWeight: FontWeight.w400,
height: 1.3,
),
),
const SizedBox(height: 12),
// Vertical line
Padding(
padding: const EdgeInsets.only(left: 40),
child: Container(
width: 2,
height: 40,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
Text(
"Let's check your tasks",
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 18,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 8),
// Arrow pointing down to tasks tab (2nd icon)
Padding(
padding: const EdgeInsets.only(left: 85),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using a hardcoded padding value left: 85 is not responsive and will cause UI issues on different screen sizes. The arrow will not be positioned correctly under the 'Tasks' tab. Please use a responsive approach, for example by using MediaQuery.of(context).size.width to calculate the padding dynamically.

child: CustomPaint(
size: const Size(40, 70),
painter: _ArrowDownPainter(),
),
),
],
),
);
}
}

// Arrow pointing straight down
class _ArrowDownPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey.shade600
..strokeWidth = 2
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;

// Curved line going down
final path = Path();
path.moveTo(size.width * 0.5, 0);
path.quadraticBezierTo(
size.width * 0.6,
size.height * 0.5,
size.width * 0.5,
size.height * 0.85,
);

canvas.drawPath(path, paint);

// Arrow head pointing down
final arrowPath = Path();
arrowPath.moveTo(size.width * 0.5 - 6, size.height * 0.72);
arrowPath.lineTo(size.width * 0.5, size.height * 0.85);
arrowPath.lineTo(size.width * 0.5 + 6, size.height * 0.72);

canvas.drawPath(arrowPath, paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
3 changes: 3 additions & 0 deletions app/lib/pages/conversations/widgets/wrapped_banner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class _WrappedBannerState extends State<WrappedBanner> with SingleTickerProvider

@override
Widget build(BuildContext context) {
// Hide wrapped banner for now
return const SizedBox.shrink();

// Don't show banner if user has already viewed their wrapped
if (SharedPreferencesUtil().hasViewedWrapped2025) {
return const SizedBox.shrink();
Expand Down
9 changes: 9 additions & 0 deletions app/lib/pages/memories/page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/memory.dart';
import 'package:omi/providers/home_provider.dart';
import 'package:omi/providers/memories_provider.dart';
Expand All @@ -13,6 +14,7 @@ import 'package:shimmer/shimmer.dart';
import 'widgets/memory_edit_sheet.dart';
import 'widgets/memory_item.dart';
import 'widgets/memory_dialog.dart';
import 'widgets/memories_onboarding_widget.dart';

import 'package:omi/utils/l10n_extensions.dart';

Expand Down Expand Up @@ -311,6 +313,10 @@ class MemoriesPageState extends State<MemoriesPage> with AutomaticKeepAliveClien
height: 44,
child: ElevatedButton(
onPressed: () {
// Mark onboarding as completed when brain map is opened
if (!SharedPreferencesUtil().hasOpenedBrainMap) {
SharedPreferencesUtil().hasOpenedBrainMap = true;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MemoryGraphPage(),
Expand Down Expand Up @@ -351,6 +357,9 @@ class MemoriesPageState extends State<MemoriesPage> with AutomaticKeepAliveClien
),
),
),
// Onboarding text widget
if (!SharedPreferencesUtil().isHomeOnboardingCompleted)
const SliverToBoxAdapter(child: MemoriesOnboardingWidget()),
if (provider.filteredMemories.isEmpty)
SliverFillRemaining(
child: Center(
Expand Down
Loading