Skip to content
Merged
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
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
<meta-data android:name="com.posthog.posthog.POSTHOG_HOST" android:value="https://us.i.posthog.com" />
<meta-data android:name="com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS" android:value="true" />
<meta-data android:name="com.posthog.posthog.DEBUG" android:value="true" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_notification" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion lib/core/common/widgets/any_step_app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ class AnyStepAppBar extends StatelessWidget implements PreferredSizeWidget {
super.key,
this.title,
this.actions,
this.leading,
this.showBackArrow = true,
this.surfaceTint,
this.bottom,
});

final Widget? title;
final List<Widget>? actions;
final Widget? leading;
final bool showBackArrow;
final Color? surfaceTint;
final PreferredSizeWidget? bottom;
Expand All @@ -25,7 +27,7 @@ class AnyStepAppBar extends StatelessWidget implements PreferredSizeWidget {
backgroundColor: Colors.transparent,
elevation: 0,
actions: actions,
leading: null,
leading: leading,
// titleTextStyle: AnyStepTextStyles.title.copyWith(
// color: Theme.of(context).colorScheme.onSurface,
// ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,90 +43,88 @@ class _DashboardCalendarCardState extends ConsumerState<DashboardCalendarCard> {
child: Card(
child: Padding(
padding: const EdgeInsets.all(AnyStepSpacing.md16),
child: eventsAsync.when(
loading: () => SizedBox(
height: 380,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SizedBox(height: AnyStepSpacing.md14),
Center(child: AnyStepShimmer(height: 24, width: 140)),
SizedBox(height: AnyStepSpacing.sm10),
AnyStepShimmer(height: 280),
SizedBox(height: AnyStepSpacing.md12),
AnyStepShimmer(height: 16, width: 220),
SizedBox(height: AnyStepSpacing.sm8),
AnyStepShimmer(height: 16, width: 180),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TableCalendar<EventModel>(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(day, _selectedDay),
eventLoader: (day) {
final data = eventsAsync.value;
if (data == null) return const [];
final key = DateTime(day.year, day.month, day.day);
return data.where((event) {
return event.startTime.year == key.year &&
event.startTime.month == key.month &&
event.startTime.day == key.day;
}).toList();
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onPageChanged: (focusedDay) {
setState(() => _focusedDay = focusedDay);
},
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withAlpha(80),
shape: BoxShape.circle,
),
selectedDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
markerDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
markersMaxCount: 3,
),
headerStyle: HeaderStyle(
titleCentered: true,
formatButtonVisible: false,
leftChevronIcon: Icon(
Icons.chevron_left,
color: Theme.of(context).iconTheme.color,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color,
),
),
),
),
error: (e, st) => SizedBox(height: 380, child: Center(child: Text(loc.failedToLoad))),
data: (events) {
final eventsByDay = <DateTime, List<EventModel>>{};
for (final event in events) {
final dateKey = DateTime(
event.startTime.year,
event.startTime.month,
event.startTime.day,
);
eventsByDay.putIfAbsent(dateKey, () => []).add(event);
}
final selected = _selectedDay ?? _focusedDay;
final selectedKey = DateTime(selected.year, selected.month, selected.day);
final selectedEvents = eventsByDay[selectedKey] ?? const [];
const SizedBox(height: AnyStepSpacing.md12),
eventsAsync.when(
loading: () => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
AnyStepShimmer(height: 16, width: 220),
SizedBox(height: AnyStepSpacing.sm8),
AnyStepShimmer(height: 16, width: 180),
],
),
error: (_, __) => Text(loc.failedToLoad),
data: (events) {
final eventsByDay = <DateTime, List<EventModel>>{};
for (final event in events) {
final dateKey = DateTime(
event.startTime.year,
event.startTime.month,
event.startTime.day,
);
eventsByDay.putIfAbsent(dateKey, () => []).add(event);
}
final selected = _selectedDay ?? _focusedDay;
final selectedKey = DateTime(selected.year, selected.month, selected.day);
final selectedEvents = eventsByDay[selectedKey] ?? const [];

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TableCalendar<EventModel>(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(day, _selectedDay),
eventLoader: (day) {
final key = DateTime(day.year, day.month, day.day);
return eventsByDay[key] ?? const [];
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onPageChanged: (focusedDay) {
setState(() => _focusedDay = focusedDay);
},
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withAlpha(80),
shape: BoxShape.circle,
),
selectedDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
markerDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
markersMaxCount: 3,
),
headerStyle: HeaderStyle(
titleCentered: true,
formatButtonVisible: false,
leftChevronIcon: Icon(
Icons.chevron_left,
color: Theme.of(context).iconTheme.color,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color,
),
),
),
const SizedBox(height: AnyStepSpacing.md12),
if (selectedEvents.isEmpty)
Container(
if (selectedEvents.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AnyStepSpacing.md12),
decoration: BoxDecoration(
Expand All @@ -137,21 +135,21 @@ class _DashboardCalendarCardState extends ConsumerState<DashboardCalendarCard> {
loc.dashboardNoEventsCalendar,
style: Theme.of(context).textTheme.bodySmall,
),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final event in selectedEvents.take(3))
Padding(
padding: const EdgeInsets.only(bottom: AnyStepSpacing.sm8),
child: _EventRow(event: event),
),
],
),
],
);
},
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final event in selectedEvents.take(3))
Padding(
padding: const EdgeInsets.only(bottom: AnyStepSpacing.sm8),
child: _EventRow(event: event),
),
],
);
},
),
],
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,22 @@ class _EventDetailScreenState extends ConsumerState<EventDetailScreen> {
final userAsync = ref.watch(currentUserStreamProvider);
final loc = AppLocalizations.of(context);

final leading = context.canPop()
? null
: IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () {
context.go(EventFeedScreen.path);
},
);

return eventAsync.when(
loading: () => AnyStepScaffold(
appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle)),
appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle), leading: leading),
body: const Center(child: AnyStepLoadingIndicator()),
),
error: (e, st) => AnyStepScaffold(
appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle)),
appBar: AnyStepAppBar(title: Text(loc.eventDetailTitle), leading: leading),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(getEventProvider(widget.id)),
child: ScrollableCenteredContent(child: AnyStepErrorWidget()),
Expand Down Expand Up @@ -103,10 +112,7 @@ class _EventDetailScreenState extends ConsumerState<EventDetailScreen> {
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: _EventDetailMenuAction.share,
child: Text(loc.shareAction),
),
PopupMenuItem(value: _EventDetailMenuAction.share, child: Text(loc.shareAction)),
if (!isPast)
PopupMenuItem(
value: _EventDetailMenuAction.addToCalendar,
Expand All @@ -125,6 +131,7 @@ class _EventDetailScreenState extends ConsumerState<EventDetailScreen> {
appBar: AnyStepAppBar(
title: Text(loc.eventDetailTitle),
actions: isPast ? null : actions,
leading: leading,
),
body: MaxWidthContainer(
child: SafeArea(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,8 @@ class _AnyStepAddressFormState extends ConsumerState<AnyStepAddressForm> {
_debounce?.cancel();
_addressId = null;
form.fields[widget.addressIdFieldName]?.didChange(null);
if (widget.showNameField) {
final nameValue = parsed.name ?? prediction.mainText ?? prediction.description;
form.fields[widget.nameFieldName]?.didChange(nameValue);
}
form.fields['street']?.didChange(parsed.street);
final streetText = parsed.street.isNotEmpty ? parsed.street : prediction.mainText ?? '';
form.fields['street']?.didChange(streetText);
form.fields['streetSecondary']?.didChange(parsed.streetSecondary);
form.fields['city']?.didChange(parsed.city);
form.fields['state']?.didChange(parsed.state);
Expand All @@ -311,7 +308,7 @@ class _AnyStepAddressFormState extends ConsumerState<AnyStepAddressForm> {
_placeName = parsed.name;
_latitude = parsed.latitude;
_longitude = parsed.longitude;
_streetController.text = parsed.street.isNotEmpty ? parsed.street : prediction.description;
_streetController.text = streetText;
_streetController.selection = TextSelection.fromPosition(
TextPosition(offset: _streetController.text.length),
);
Expand Down
33 changes: 17 additions & 16 deletions lib/core/features/notifications/data/notification_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import 'package:flutter/foundation.dart';

part 'notification_repository.g.dart';

/// TODO:
/// 1. Implement IRepository in NotificationRepository
/// 2. Create a provider to fetch and paginate user notifications (scoped by user_id == usermodel.id aka authState sub), sorted by created at date
/// 3. Create a listview similar to event feed. Handle showing read and unread states.
/// 4. When the user clicks on a notification it should open a notification detail screen and automatically mark the notification as read.
///
/// Entrypoint: Dashboard appbar action IconButton, pushes route. Only visible for authenticated users.

const String _eventIdKey = 'event_id';
String _eventDetailPath(int id) => '/events/$id';

Expand All @@ -21,9 +29,9 @@ class NotificationRepository {
required FirebaseMessaging messaging,
required GoRouter router,
required SupabaseClient supabase,
}) : _messaging = messaging,
_router = router,
_supabase = supabase;
}) : _messaging = messaging,
_router = router,
_supabase = supabase;

final FirebaseMessaging _messaging;
final GoRouter _router;
Expand Down Expand Up @@ -95,10 +103,7 @@ class NotificationRepository {
content: Text(body.isEmpty ? title : '$title\n$body'),
action: eventId == null
? null
: SnackBarAction(
label: 'Open',
onPressed: () => _router.go(_eventDetailPath(eventId)),
),
: SnackBarAction(label: 'Open', onPressed: () => _router.go(_eventDetailPath(eventId))),
),
);
}
Expand Down Expand Up @@ -134,20 +139,16 @@ class NotificationRepository {
if (user == null) return;
try {
final platform = _platformLabel();
await _supabase.from('user_fcm_tokens').upsert(
{
'user_id': user.id,
'token': token,
'platform': platform,
},
onConflict: 'token',
);
await _supabase.from('user_fcm_tokens').upsert({
'user_id': user.id,
'token': token,
'platform': platform,
}, onConflict: 'token');
Log.i('Synced FCM token for user ${user.id} ($platform)');
} catch (e, st) {
Log.e('Failed to sync FCM token', e, st);
}
}

}

String _platformLabel() {
Expand Down