Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a customizable card in the dashboard #853

Merged
merged 26 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ffbe0d3
Add broadcast card logic (WIP)
eneiss Sep 23, 2022
124ed42
WIP broadcast card UI & logic w/ remote config
eneiss Jan 8, 2023
f629348
Fix card order in dashboard+ config default
eneiss Jan 8, 2023
8ea729e
Set card busy when model loading
eneiss Jan 8, 2023
cbf7162
WIP add title & toggle remote configs
eneiss Feb 3, 2023
03f0691
Merge branch 'master' into feature/dashboard_broadcast_card
HugoMigner Aug 1, 2023
ef56097
Add remote config based color
HugoMigner Aug 1, 2023
a78ff38
Use color from remote config, or default
HugoMigner Aug 3, 2023
9a7bbc3
Fix color variable and text color
HugoMigner Aug 3, 2023
f37930f
Add reorder broadcast
HugoMigner Aug 9, 2023
4fb98af
Add custom icon, onClick event and reorderable/hide functions
HugoMigner Sep 19, 2023
2947a69
Fix cards not appearing
HugoMigner Sep 20, 2023
a40be51
Tests
HugoMigner Sep 21, 2023
6c664d5
Cleanup
HugoMigner Sep 21, 2023
66305c2
Merge branch 'master' into feature/dashboard_broadcast_card
HugoMigner Sep 21, 2023
55714bd
[BOT] Applying version.
HugoMigner Sep 21, 2023
6e16d07
Fix tests
HugoMigner Sep 21, 2023
4be9dea
Merge branch 'feature/dashboard_broadcast_card' of https://github.com…
HugoMigner Sep 21, 2023
97d87c9
Merge branch 'master' into feature/dashboard_broadcast_card
HugoMigner Sep 21, 2023
0b996c6
Merge branch 'master' into feature/dashboard_broadcast_card
HugoMigner Sep 25, 2023
84b36e7
[BOT] Applying version.
HugoMigner Sep 25, 2023
c926bed
Check for broadcast change while card is dismissed
HugoMigner Sep 25, 2023
fd7dcb7
Fix tests
HugoMigner Sep 25, 2023
165a2f4
Fix tests again
HugoMigner Sep 25, 2023
251ab53
Fix tests pdv
HugoMigner Sep 25, 2023
d584cea
Fix tests real
HugoMigner Sep 25, 2023
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
1 change: 1 addition & 0 deletions lib/core/constants/preferences_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum PreferencesFlag {
discoveryMore,

// Dashboard flags
broadcastCard,
aboutUsCard,
scheduleCard,
progressBarCard,
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
41 changes: 39 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 @@ -39,6 +40,8 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
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 +61,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 @@ -189,6 +200,7 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {

Future loadDataAndUpdateWidget() async {
return Future.wait([
futureToRunBroadcast(),
futureToRunGrades(),
futureToRunSessionProgressBar(),
futureToRunSchedule()
Expand Down Expand Up @@ -282,13 +294,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 @@ -535,4 +547,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.25.3+1
version: 4.26.0+1

environment:
sdk: ">=2.10.0 <3.0.0"
Expand Down
36 changes: 23 additions & 13 deletions test/managers/settings_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -440,17 +440,20 @@ void main() {

// Cards
final Map<PreferencesFlag, int> expected = {
PreferencesFlag.aboutUsCard: 0,
PreferencesFlag.scheduleCard: 1,
PreferencesFlag.progressBarCard: 2,
PreferencesFlag.gradesCard: 3
PreferencesFlag.broadcastCard: 0,
PreferencesFlag.aboutUsCard: 1,
PreferencesFlag.scheduleCard: 2,
PreferencesFlag.progressBarCard: 3,
PreferencesFlag.gradesCard: 4
};

expect(
await manager.getDashboard(),
expected,
);

verify(preferencesService.getInt(PreferencesFlag.broadcastCard))
.called(1);
verify(preferencesService.getInt(PreferencesFlag.aboutUsCard))
.called(1);
verify(preferencesService.getInt(PreferencesFlag.scheduleCard))
Expand All @@ -464,37 +467,44 @@ void main() {
});

test("validate the loading of the cards", () async {
PreferencesServiceMock.stubGetInt(
preferencesService as PreferencesServiceMock,
PreferencesFlag.broadcastCard,
toReturn: 0);
PreferencesServiceMock.stubGetInt(
preferencesService as PreferencesServiceMock,
PreferencesFlag.aboutUsCard,
// ignore: avoid_redundant_argument_values
toReturn: 1);
toReturn: 2);
PreferencesServiceMock.stubGetInt(
preferencesService as PreferencesServiceMock,
PreferencesFlag.scheduleCard,
toReturn: 2);
toReturn: 3);
PreferencesServiceMock.stubGetInt(
preferencesService as PreferencesServiceMock,
PreferencesFlag.progressBarCard,
toReturn: 0);
// ignore: avoid_redundant_argument_values
toReturn: 1);
PreferencesServiceMock.stubGetInt(
preferencesService as PreferencesServiceMock,
PreferencesFlag.gradesCard,
toReturn: 3);
toReturn: 4);

// Cards
final Map<PreferencesFlag, int> expected = {
PreferencesFlag.aboutUsCard: 1,
PreferencesFlag.scheduleCard: 2,
PreferencesFlag.progressBarCard: 0,
PreferencesFlag.gradesCard: 3
PreferencesFlag.broadcastCard: 0,
PreferencesFlag.aboutUsCard: 2,
PreferencesFlag.scheduleCard: 3,
PreferencesFlag.progressBarCard: 1,
PreferencesFlag.gradesCard: 4
};

expect(
await manager.getDashboard(),
expected,
);

verify(preferencesService.getInt(PreferencesFlag.broadcastCard))
.called(1);
verify(preferencesService.getInt(PreferencesFlag.aboutUsCard))
.called(1);
verify(preferencesService.getInt(PreferencesFlag.scheduleCard))
Expand Down
Loading
Loading