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

[Hello] News infinite scroll #937

Merged
merged 20 commits into from
Mar 18, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/release-workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.3.x'
flutter-version: '3.x'
channel: 'stable'
cache: true
- name: Setup Fastlane
Expand Down
6 changes: 5 additions & 1 deletion l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@
"ets_gus": "GUS",
"ets_papercut_title":"PaperCut",

"news_title" : "Announcement",
"news_title" : "News",
"news_error_not_found_title" : "Oh oh!",
"news_error_not_found" : "Something went wrong while trying to retrieve the news. Please try again later.",
"news_no_more_card_title": "You're all set!",
"news_no_more_card": "You have reached the end of the news list. Come back another time for more news!",
"news_details_title" : "News",
"news_event_date": "Event date",

Expand Down
4 changes: 4 additions & 0 deletions l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@
"news_title" : "Annonces",
"news_details_title" : "Annonce",
"news_event_date": "Date de l'événement",
"news_error_not_found_title" : "Oh oh!",
"news_error_not_found" : "Une erreur est survenue lors de la récupération des annonces. Veuillez réessayer plus tard.",
"news_no_more_card_title": "Tu es à jour!",
"news_no_more_card": "Vous avez atteint la fin des annonces. Revenez plus tard pour voir les prochaines annonces!",

"report_news": "Pourquoi reportez-vous cette annonce ?",
"report_as": "Signaler comme",
Expand Down
3 changes: 1 addition & 2 deletions lib/core/constants/quick_links.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ List<QuickLink> quickLinks(AppIntl intl) => [
id: 8,
name: intl.ets_gus,
image: SvgPicture.asset('assets/images/ic_gus_red.svg',
colorFilter: const ColorFilter.mode(
AppTheme.etsLightRed, BlendMode.srcIn)),
color: AppTheme.etsLightRed),
link: 'https://gus.etsmtl.ca/c2atom/mobile/login'),
QuickLink(
id: 9,
Expand Down
144 changes: 7 additions & 137 deletions lib/core/managers/news_repository.dart
Original file line number Diff line number Diff line change
@@ -1,155 +1,25 @@
// Dart imports:
import 'dart:convert';

// Flutter imports:
import 'package:flutter/material.dart';
import 'package:ets_api_clients/clients.dart';

// Package imports:
import 'package:logger/logger.dart';
import 'package:ets_api_clients/models.dart';

// Project imports:
import 'package:notredame/core/managers/cache_manager.dart';
import 'package:notredame/core/models/news.dart';
import 'package:notredame/core/services/networking_service.dart';
import 'package:notredame/core/utils/cache_exception.dart';
import 'package:notredame/locator.dart';

/// Repository to access all the news
class NewsRepository {
static const String tag = "NewsRepository";

@visibleForTesting
static const String newsCacheKey = "newsCache";

final Logger _logger = locator<Logger>();

/// Cache manager to access and update the cache.
final CacheManager _cacheManager = locator<CacheManager>();

/// Used to verify if the user has connectivity
final NetworkingService _networkingService = locator<NetworkingService>();

/// List of the news with 3 test news.
List<News>? _news = <News>[
News(
id: 1,
title: "Merci à McGill Robotics pour l’invitation au RoboHacks 2023!",
description:
"Le club scientifique qui conceptualise un robot de recherche et secourisme recrute pour ses nouveaux projets! Une rencontre d’information est prévue le mercredi 13 octobre 2021 à 17h au local D-5023.Viens nous rencontrer pour en savoir plus sur notre prochaine mission de secourisme et faire partie de l’équipe!Le club scientifique qui conceptualise un robot de recherche et secourisme recrute pour ses nouveaux projets! Une rencontre d’information est prévue le mercredi 13 octobre 2021 à 17h au local D-5023.Viens nous rencontrer pour en savoir plus sur notre prochaine mission de secourisme et faire partie de l’équipe!Le club scientifique qui conceptualise un robot de recherche et secourisme recrute pour ses nouveaux projets! Une rencontre d’information est prévue le mercredi 13 octobre 2021 à 17h au local D-5023.Viens nous rencontrer pour en savoir plus sur notre prochaine mission de secourisme et faire partie de l’équipe!Le club scientifique qui conceptualise un robot de recherche et secourisme recrute pour ses nouveaux projets! Une rencontre d’information est prévue le mercredi 13 octobre 2021 à 17h au local D-5023.Viens nous rencontrer pour en savoir plus sur notre prochaine mission de secourisme et faire partie de l’équipe!",
author: "Capra",
avatar: "https://picsum.photos/200/200",
activity: "Club scientifique",
publishedDate: DateTime.now(),
eventStartDate: DateTime.now().add(const Duration(days: 2)),
eventEndDate: DateTime.now().add(const Duration(days: 5)),
image: "https://picsum.photos/400/200",
tags: [
"Robotique",
"Programmation",
"Intelligence artificielle",
"Compétition",
],
),
News(
id: 2,
title: "Compétition de développement mobile",
description:
"AMC est une compétition de développement mobile organisée par ApplETS, un club étudiant de l'ÉTS. La compétition à lieu du 27 au 28 janvier 2024. Que vous soyez un étudiant universitaire ou collégial, novice ou expérimenté en développement, cette compétition est l'occasion idéale de repousser vos limites, d'apprendre des autres et de montrer votre talent dans le monde de la technologie mobile.",
author: "App|ETS",
avatar: "https://picsum.photos/200/200",
activity: "Club scientifique",
publishedDate: DateTime.now(),
eventStartDate: DateTime.now().add(const Duration(days: 10)),
eventEndDate: DateTime.now().add(const Duration(days: 11)),
image: "https://picsum.photos/400/200",
tags: [
"Compétition",
"Développement mobile",
],
),
News(
id: 3,
title: "Test 3",
description: "Test 3 description",
author: "Jean-Guy Tremblay",
avatar: "https://picsum.photos/200/200",
activity: "Service à la vie étudiante",
publishedDate: DateTime.now(),
eventStartDate: DateTime.now().add(const Duration(days: 3)),
image: "https://picsum.photos/400/200",
tags: ["5 @ 7", "Vie étudiante", "Activité"],
),
];

List<News>? get news => _news;
final HelloAPIClient _helloApiClient = locator<HelloAPIClient>();

/// Get and update the list of news.
/// After fetching the news from the [?] the [CacheManager]
/// is updated with the latest version of the news.
Future<List<News>?> getNews({bool fromCacheOnly = false}) async {
// Force fromCacheOnly mode when user has no connectivity
if (!(await _networkingService.hasConnectivity())) {
// ignore: parameter_assignments
fromCacheOnly = true;
}

// Load the news from the cache if the list doesn't exist
if (_news == null) {
await getNewsFromCache();
}

if (fromCacheOnly) {
return _news;
}

final List<News> fetchedNews = fetchNewsFromAPI();

_news ??= [];

// Update the list of news to avoid duplicate news
for (final News news in fetchedNews) {
if (_news?.contains(news) ?? false) {
_news?.add(news);
}
}

try {
// Update cache
_cacheManager.update(newsCacheKey, jsonEncode(_news));
} on CacheException catch (_) {
// Do nothing, the caching will retry later and the error has been logged by the [CacheManager]
_logger.e(
"$tag - getNews: exception raised will trying to update the cache.");
}

return _news;
}

Future<void> getNewsFromCache() async {
_news = [];
try {
final List responseCache =
jsonDecode(await _cacheManager.get(newsCacheKey)) as List<dynamic>;

// Build list of news loaded from the cache.
_news = responseCache
.map((e) => News.fromJson(e as Map<String, dynamic>))
.toList();

_logger.d(
"$tag - getNewsFromCache: ${_news?.length} news loaded from cache");
} on CacheException catch (_) {
_logger.e(
"$tag - getNewsFromCache: exception raised will trying to load news from cache.");
}
}

// TODO : Fetch news from the API
List<News> fetchNewsFromAPI() {
final List<News> fetchedNews = [];

_logger.d("$tag - getNews: fetched ${fetchedNews.length} news.");

return fetchedNews;
Future<PaginatedNews?> getNews({int pageNumber = 1, int pageSize = 3}) async {
final PaginatedNews pagination = await _helloApiClient.getEvents(
pageNumber: pageNumber, pageSize: pageSize);
return pagination;
}
}
59 changes: 0 additions & 59 deletions lib/core/models/news.dart

This file was deleted.

6 changes: 0 additions & 6 deletions lib/core/services/launch_url_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import 'package:notredame/core/managers/settings_manager.dart';
import 'package:notredame/locator.dart';
import 'package:notredame/ui/utils/app_theme.dart';

// Managers

// UTILS

// OTHER

class LaunchUrlService {
final SettingsManager settingsManager = locator<SettingsManager>();

Expand Down
2 changes: 1 addition & 1 deletion lib/core/viewmodels/news_details_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import 'dart:ui';

// Package imports:
import 'package:ets_api_clients/models.dart';
import 'package:stacked/stacked.dart';

// Project imports:
import 'package:notredame/core/models/news.dart';
import 'package:notredame/ui/utils/app_theme.dart';

class NewsDetailsViewModel extends FutureViewModel<News> {
Expand Down
66 changes: 21 additions & 45 deletions lib/core/viewmodels/news_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -1,65 +1,41 @@
// Package imports:
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:stacked/stacked.dart';
import 'package:ets_api_clients/models.dart';

// Project imports:
import 'package:notredame/core/managers/news_repository.dart';
import 'package:notredame/core/models/news.dart';
import 'package:notredame/locator.dart';

class NewsViewModel extends FutureViewModel<List<News>> {
class NewsViewModel extends BaseViewModel implements Initialisable {
/// Load the events
final NewsRepository _newsRepository = locator<NewsRepository>();

/// Localization class of the application.
final AppIntl _appIntl;

/// News list
List<News>? _news = [];

/// Return the list of all the news.
List<News> get news {
_news = [];

// Build the list of news
_news?.addAll(_newsRepository.news ?? []);

return _news ?? [];
}

NewsViewModel({required AppIntl intl}) : _appIntl = intl;
final PagingController<int, News> pagingController =
PagingController(firstPageKey: 1);

bool isLoadingEvents = false;

@override
Future<List<News>> futureToRun() async {
try {
setBusyForObject(isLoadingEvents, true);
_news = await _newsRepository.getNews(fromCacheOnly: true);
notifyListeners();
} catch (e) {
onError(e);
} finally {
setBusyForObject(isLoadingEvents, false);
}

return news;
}

@override
// ignore: type_annotate_public_apis
void onError(error) {
Fluttertoast.showToast(msg: _appIntl.error);
void initialise() {
// This will be called when init state cycle runs
pagingController.addPageRequestListener((pageKey) {
fetchPage(pageKey);
});
}

Future refresh() async {
Future<void> fetchPage(int pageNumber) async {
try {
setBusyForObject(isLoadingEvents, true);
_newsRepository.getNews();
notifyListeners();
} on Exception catch (error) {
onError(error);
final pagination = await _newsRepository.getNews(pageNumber: pageNumber);
final isLastPage = pagination?.totalPages == pageNumber;
if (isLastPage) {
pagingController.appendLastPage(pagination?.news ?? []);
} else {
final nextPageKey = pageNumber + 1;
pagingController.appendPage(pagination?.news ?? [], nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
}
1 change: 1 addition & 0 deletions lib/locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ void setupLocator() {
// Other
locator.registerLazySingleton(() => SignetsAPIClient());
locator.registerLazySingleton(() => MonETSAPIClient());
locator.registerLazySingleton(() => HelloAPIClient());
locator.registerLazySingleton(() => Logger());
}
1 change: 0 additions & 1 deletion lib/ui/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import 'package:notredame/ui/views/settings_view.dart';
import 'package:notredame/ui/views/startup_view.dart';
import 'package:notredame/ui/views/student_view.dart';
import 'package:notredame/ui/widgets/link_web_view.dart';
import 'package:notredame/core/models/news.dart';

Route<dynamic> generateRoute(RouteSettings routeSettings) {
switch (routeSettings.name) {
Expand Down
Loading
Loading