Skip to content

Commit

Permalink
Add a customizable card in the dashboard (#853)
Browse files Browse the repository at this point in the history
* Add broadcast card logic (WIP)

* WIP broadcast card UI & logic w/ remote config

* Fix card order in dashboard+ config default

Fix card order in dashboard and prevent duplicates from appearing
Add default values for fr & en remote configs

* Set card busy when model loading

And minor code improvements

* WIP add title & toggle remote configs

* Add remote config based color

* Use color from remote config, or default

* Fix color variable and text color

* Add reorder broadcast

* Add custom icon, onClick event and reorderable/hide functions

* Fix cards not appearing

* Tests

* Cleanup

* [BOT] Applying version.

* Fix tests

* [BOT] Applying version.

* Check for broadcast change while card is dismissed

* Fix tests

* Fix tests again

* Fix tests pdv

* Fix tests real

---------

Co-authored-by: Emma <[email protected]>
Co-authored-by: HugoMigner <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2023
1 parent db09c2a commit e5a8dd9
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 84 deletions.
2 changes: 2 additions & 0 deletions lib/core/constants/preferences_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ enum PreferencesFlag {
discoveryMore,

// Dashboard flags
broadcastCard,
aboutUsCard,
scheduleCard,
progressBarCard,
gradesCard,
progressBarText,
broadcastChange,

// Rating flag
ratingTimer,
Expand Down
12 changes: 10 additions & 2 deletions lib/core/managers/settings_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class SettingsManager with ChangeNotifier {
Future<Map<PreferencesFlag, int>> getDashboard() async {
final Map<PreferencesFlag, int> dashboard = {};

final broadcastCardIndex =
await _preferencesService.getInt(PreferencesFlag.broadcastCard) ??
getDefaultCardIndex(PreferencesFlag.broadcastCard);

dashboard.putIfAbsent(
PreferencesFlag.broadcastCard, () => broadcastCardIndex);

final aboutUsIndex =
await _preferencesService.getInt(PreferencesFlag.aboutUsCard) ??
getDefaultCardIndex(PreferencesFlag.aboutUsCard);
Expand Down Expand Up @@ -268,8 +275,9 @@ class SettingsManager with ChangeNotifier {
}

/// Get the default index of each card
int getDefaultCardIndex(PreferencesFlag flag) =>
flag.index - PreferencesFlag.aboutUsCard.index;
int getDefaultCardIndex(PreferencesFlag flag) {
return flag.index - PreferencesFlag.broadcastCard.index;
}

bool get calendarViewSetting => _remoteConfigService.scheduleListViewDefault;
}
60 changes: 59 additions & 1 deletion lib/core/services/remote_config_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,32 @@ import 'package:notredame/locator.dart';

/// Manage the analytics of the application
class RemoteConfigService {
static const String tag = "RemoteConfigService";
static const _serviceIsDown = "service_is_down";

// dashboard message remote config keys
static const _dashboardMsgToggle = "dashboard_message_toggle";
static const _dashboardMsgFr = "dashboard_message_fr";
static const _dashboardMsgEn = "dashboard_message_en";
static const _dashboardMsgTitleFr = "dashboard_message_title_fr";
static const _dashboardMsgTitleEn = "dashboard_message_title_en";
static const _dashboardMsgColor = "dashboard_message_color";
static const _dashboardMsgUrl = "dashboard_message_url";
static const _dashboardMsgType = "dashboard_message_type";

static const _scheduleListViewDefault = "schedule_list_view_default";
final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance;
final defaults = <String, dynamic>{
_serviceIsDown: false,
_dashboardMsgFr: "",
_dashboardMsgEn: "",
_dashboardMsgTitleFr: "",
_dashboardMsgTitleEn: "",
_dashboardMsgColor: "",
_dashboardMsgUrl: "",
_dashboardMsgType: "",
_scheduleListViewDefault: true
};
static const String tag = "RemoteConfigService";

Future initialize() async {
await _remoteConfig.setDefaults(defaults);
Expand All @@ -27,11 +45,51 @@ class RemoteConfigService {
return _remoteConfig.getBool(_serviceIsDown);
}

bool get dashboardMessageActive {
fetch();
return _remoteConfig.getBool(_dashboardMsgToggle);
}

bool get scheduleListViewDefault {
fetch();
return _remoteConfig.getBool(_scheduleListViewDefault);
}

String get dashboardMessageFr {
fetch();
return _remoteConfig.getString(_dashboardMsgFr);
}

String get dashboardMessageEn {
fetch();
return _remoteConfig.getString(_dashboardMsgEn);
}

String get dashboardMessageTitleFr {
fetch();
return _remoteConfig.getString(_dashboardMsgTitleFr);
}

String get dashboardMessageTitleEn {
fetch();
return _remoteConfig.getString(_dashboardMsgTitleEn);
}

String get dashboardMsgColor {
fetch();
return _remoteConfig.getString(_dashboardMsgColor);
}

String get dashboardMsgUrl {
fetch();
return _remoteConfig.getString(_dashboardMsgUrl);
}

String get dashboardMsgType {
fetch();
return _remoteConfig.getString(_dashboardMsgType);
}

Future<void> fetch() async {
final AnalyticsService analyticsService = locator<AnalyticsService>();
try {
Expand Down
65 changes: 63 additions & 2 deletions lib/core/viewmodels/dashboard_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:notredame/core/services/siren_flutter_service.dart';
import 'package:notredame/core/services/preferences_service.dart';
import 'package:notredame/core/services/analytics_service.dart';
import 'package:notredame/core/services/app_widget_service.dart';
import 'package:notredame/core/services/remote_config_service.dart';

// MODEL
import 'package:notredame/core/models/widget_models.dart';
Expand All @@ -36,9 +37,12 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
static const String tag = "DashboardViewModel";

final SettingsManager _settingsManager = locator<SettingsManager>();
final PreferencesService _preferencesService = locator<PreferencesService>();
final CourseRepository _courseRepository = locator<CourseRepository>();
final AnalyticsService _analyticsService = locator<AnalyticsService>();
final AppWidgetService _appWidgetService = locator<AppWidgetService>();
final RemoteConfigService remoteConfigService =
locator<RemoteConfigService>();

/// All dashboard displayable cards
Map<PreferencesFlag, int> _cards;
Expand All @@ -58,6 +62,14 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
/// Numbers of days elapsed and total number of days of the current session
List<int> _sessionDays = [0, 0];

/// Message to display in case of urgent/important broadcast need (Firebase
/// remote config), and the associated card title
String broadcastMessage = "";
String broadcastTitle = "";
String broadcastColor = "";
String broadcastUrl = "";
String broadcastType = "";

/// Get progress of the session
double get progress => _progress;

Expand Down Expand Up @@ -178,6 +190,8 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {

_cards = dashboard;

await checkForBroadcastChange();

getCardsToDisplay();

// load data for both grade cards & grades home screen widget
Expand All @@ -189,6 +203,7 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {

Future loadDataAndUpdateWidget() async {
return Future.wait([
futureToRunBroadcast(),
futureToRunGrades(),
futureToRunSessionProgressBar(),
futureToRunSchedule()
Expand Down Expand Up @@ -282,13 +297,13 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
void setAllCardsVisible() {
_cards.updateAll((key, value) {
_settingsManager
.setInt(key, key.index - PreferencesFlag.aboutUsCard.index)
.setInt(key, key.index - PreferencesFlag.broadcastCard.index)
.then((value) {
if (!value) {
Fluttertoast.showToast(msg: _appIntl.error);
}
});
return key.index - PreferencesFlag.aboutUsCard.index;
return key.index - PreferencesFlag.broadcastCard.index;
});

getCardsToDisplay();
Expand Down Expand Up @@ -316,6 +331,27 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
_analyticsService.logEvent(tag, "Restoring cards");
}

Future<void> checkForBroadcastChange() async {
final broadcastChange =
await _preferencesService.getString(PreferencesFlag.broadcastChange) ??
"";
if (broadcastChange != remoteConfigService.dashboardMessageEn) {
// Update pref
_preferencesService.setString(PreferencesFlag.broadcastChange,
remoteConfigService.dashboardMessageEn);
if (_cards[PreferencesFlag.broadcastCard] < 0) {
_cards.updateAll((key, value) {
if (value >= 0) {
return value + 1;
} else {
return value;
}
});
_cards[PreferencesFlag.broadcastCard] = 0;
}
}
}

Future<List<Session>> futureToRunSessionProgressBar() async {
String progressBarText =
await _settingsManager.getString(PreferencesFlag.progressBarText);
Expand Down Expand Up @@ -535,4 +571,29 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
PreferencesFlag.updateAskedVersion, storeVersion.toString());
}
}

Future<void> futureToRunBroadcast() async {
setBusyForObject(broadcastMessage, true);
setBusyForObject(broadcastTitle, true);
setBusyForObject(broadcastColor, true);
setBusyForObject(broadcastUrl, true);
setBusyForObject(broadcastType, true);

if (_appIntl.localeName == "fr") {
broadcastMessage = remoteConfigService.dashboardMessageFr;
broadcastTitle = remoteConfigService.dashboardMessageTitleFr;
} else {
broadcastMessage = remoteConfigService.dashboardMessageEn;
broadcastTitle = remoteConfigService.dashboardMessageTitleEn;
}
broadcastColor = remoteConfigService.dashboardMsgColor;
broadcastUrl = remoteConfigService.dashboardMsgUrl;
broadcastType = remoteConfigService.dashboardMsgType;

setBusyForObject(broadcastMessage, false);
setBusyForObject(broadcastTitle, false);
setBusyForObject(broadcastColor, false);
setBusyForObject(broadcastUrl, false);
setBusyForObject(broadcastType, false);
}
}
82 changes: 82 additions & 0 deletions lib/ui/views/dashboard_view.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// FLUTTER / DART / THIRD-PARTIES
import 'package:auto_size_text/auto_size_text.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
Expand Down Expand Up @@ -100,8 +101,16 @@ class _DashboardViewState extends State<DashboardView>
List<Widget> _buildCards(DashboardViewModel model) {
final List<Widget> cards = List.empty(growable: true);

// always try to build broadcast cart so the user doesn't miss out on
// important info if they dismissed it previously

for (final PreferencesFlag element in model.cardsToDisplay) {
switch (element) {
case PreferencesFlag.broadcastCard:
if (model.remoteConfigService.dashboardMessageActive) {
cards.add(_buildMessageBroadcastCard(model, element));
}
break;
case PreferencesFlag.aboutUsCard:
cards.add(_buildAboutUsCard(model, element));
break;
Expand Down Expand Up @@ -394,6 +403,79 @@ class _DashboardViewState extends State<DashboardView>
]),
);

Widget _buildMessageBroadcastCard(
DashboardViewModel model, PreferencesFlag flag) {
final broadcastMsgColor = Color(int.parse(model.broadcastColor));
final broadcastMsgType = model.broadcastType;
final broadcastMsgUrl = model.broadcastUrl;
return DismissibleCard(
key: UniqueKey(),
onDismissed: (DismissDirection direction) {
dismissCard(model, flag);
},
isBusy: model.busy(model.broadcastMessage),
cardColor: broadcastMsgColor,
child: Padding(
padding: const EdgeInsets.fromLTRB(17, 10, 15, 20),
child: Column(mainAxisSize: MainAxisSize.min, children: [
// title row
Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Text(model.broadcastTitle,
style: Theme.of(context).primaryTextTheme.headline6),
),
),
Align(
alignment: Alignment.centerRight,
child: InkWell(
child: getBroadcastIcon(broadcastMsgType, broadcastMsgUrl),
),
),
],
),
// main text
AutoSizeText(model.broadcastMessage ?? "",
style: Theme.of(context).primaryTextTheme.bodyText2)
]),
));
}

Widget getBroadcastIcon(String type, String url) {
switch (type) {
case "warning":
return const Icon(
Icons.warning_rounded,
color: AppTheme.lightThemeBackground,
size: 36.0,
);
case "alert":
return const Icon(
Icons.error,
color: AppTheme.lightThemeBackground,
size: 36.0,
);
case "link":
return IconButton(
onPressed: () {
Utils.launchURL(url, AppIntl.of(context));
},
icon: const Icon(
Icons.open_in_new,
color: AppTheme.lightThemeBackground,
size: 30.0,
),
);
}
return const Icon(
Icons.campaign,
color: AppTheme.lightThemeBackground,
size: 36.0,
);
}

void dismissCard(DashboardViewModel model, PreferencesFlag flag) {
model.hideCard(flag);
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 4.27.0+1
version: 4.28.0+1

environment:
sdk: ">=2.10.0 <3.0.0"
Expand Down
Loading

0 comments on commit e5a8dd9

Please sign in to comment.