Browse Source

Merge pull request #121 from provokateurin/feature/separate-notifications

Separate notifications from other apps
pull/122/head
Kate 2 years ago committed by GitHub
parent
commit
23a6253480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/neon/integration_test/screenshot_test.dart
  2. 2
      packages/neon/lib/src/apps/news/app.dart
  3. 2
      packages/neon/lib/src/apps/notifications/app.dart
  4. 22
      packages/neon/lib/src/blocs/accounts.dart
  5. 66
      packages/neon/lib/src/blocs/apps.dart
  6. 10
      packages/neon/lib/src/blocs/apps.rxb.g.dart
  7. 3
      packages/neon/lib/src/neon.dart
  8. 84
      packages/neon/lib/src/pages/home.dart
  9. 47
      packages/neon/lib/src/widgets/app_implementation_icon.dart

3
packages/neon/integration_test/screenshot_test.dart

@ -486,7 +486,8 @@ Future main() async {
), ),
); );
await prepareScreenshot(tester, binding); await prepareScreenshot(tester, binding);
await switchPage(tester, 'app-notifications'); await tester.tap(find.byKey(const Key('app-notifications')));
await tester.pumpAndSettle();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.pump(); await tester.pump();

2
packages/neon/lib/src/apps/news/app.dart

@ -71,5 +71,5 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
); );
@override @override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter; BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter;
} }

2
packages/neon/lib/src/apps/notifications/app.dart

@ -40,6 +40,6 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
); );
@override @override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) =>
appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter; appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter;
} }

22
packages/neon/lib/src/blocs/accounts.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/blocs/user_details.dart'; import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_status.dart'; import 'package:neon/src/blocs/user_status.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
@ -127,18 +128,30 @@ class AccountsBloc extends $AccountsBloc {
); );
AppsBloc getAppsBloc(final Account account) { AppsBloc getAppsBloc(final Account account) {
if (_accountsAppsBlocs[account.id] != null) { if (_appsBlocs[account.id] != null) {
return _accountsAppsBlocs[account.id]!; return _appsBlocs[account.id]!;
} }
return _accountsAppsBlocs[account.id] = AppsBloc( return _appsBlocs[account.id] = AppsBloc(
_requestManager, _requestManager,
getCapabilitiesBloc(account),
this, this,
account, account,
_allAppImplementations, _allAppImplementations,
); );
} }
CapabilitiesBloc getCapabilitiesBloc(final Account account) {
if (_capabilitiesBlocs[account.id] != null) {
return _capabilitiesBlocs[account.id]!;
}
return _capabilitiesBlocs[account.id] = CapabilitiesBloc(
_requestManager,
account.client,
);
}
UserDetailsBloc getUserDetailsBloc(final Account account) { UserDetailsBloc getUserDetailsBloc(final Account account) {
if (_userDetailsBlocs[account.id] != null) { if (_userDetailsBlocs[account.id] != null) {
return _userDetailsBlocs[account.id]!; return _userDetailsBlocs[account.id]!;
@ -176,7 +189,8 @@ class AccountsBloc extends $AccountsBloc {
late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null); late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]); late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
final _accountsAppsBlocs = <String, AppsBloc>{}; final _appsBlocs = <String, AppsBloc>{};
final _capabilitiesBlocs = <String, CapabilitiesBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{}; final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusBlocs = <String, UserStatusBloc>{}; final _userStatusBlocs = <String, UserStatusBloc>{};

66
packages/neon/lib/src/blocs/apps.dart

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:neon/src/apps/notifications/app.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart'; import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -22,6 +23,8 @@ abstract class AppsBlocStates {
BehaviorSubject<Result<List<AppImplementation>>> get appImplementations; BehaviorSubject<Result<List<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation;
BehaviorSubject<String?> get activeAppID; BehaviorSubject<String?> get activeAppID;
} }
@ -29,6 +32,7 @@ abstract class AppsBlocStates {
class AppsBloc extends $AppsBloc { class AppsBloc extends $AppsBloc {
AppsBloc( AppsBloc(
this._requestManager, this._requestManager,
this._capabilitiesBloc,
this._accountsBloc, this._accountsBloc,
this._account, this._account,
this._allAppImplementations, this._allAppImplementations,
@ -40,28 +44,31 @@ class AppsBloc extends $AppsBloc {
if (_activeAppSubject.valueOrNull != appId) { if (_activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId); _activeAppSubject.add(appId);
} }
} else if (appId == 'notifications') {
// TODO: Open notifications page
} else { } else {
debugPrint('App $appId not found'); throw Exception('App $appId not found');
} }
}); });
_appsSubject.listen((final result) { _appsSubject.listen((final result) {
if (result is ResultLoading) { if (result is ResultLoading) {
_appImplementationsSubject.add(Result.loading()); _appImplementationsSubject.add(Result.loading());
} else if (result is ResultError) { } else if (result is ResultError<List<NextcloudApp>>) {
_appImplementationsSubject.add(Result.error((result as ResultError).error)); _appImplementationsSubject.add(Result.error(result.error));
} else if (result is ResultSuccess) { } else if (result is ResultSuccess<List<NextcloudApp>>) {
_appImplementationsSubject.add( _appImplementationsSubject.add(
Result.success(_filteredAppImplementations((result as ResultSuccess<List<NextcloudApp>>).data)), Result.success(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
); );
} else if (result is ResultCached && result.data != null) { } else if (result is ResultCached<List<NextcloudApp>>) {
_appImplementationsSubject.add( _appImplementationsSubject.add(
ResultCached(_filteredAppImplementations((result as ResultCached<List<NextcloudApp>>).data)), ResultCached(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
); );
} }
final appImplementations = final appImplementations = result.data != null
result.data != null ? _filteredAppImplementations(result.data!) : <AppImplementation>[]; ? _filteredAppImplementations(result.data!.map((final a) => a.id).toList())
: <AppImplementation>[];
if (result.data != null) { if (result.data != null) {
final options = _accountsBloc.getOptions(_account); final options = _accountsBloc.getOptions(_account);
@ -84,16 +91,39 @@ class AppsBloc extends $AppsBloc {
} }
}); });
_capabilitiesBloc.capabilities.listen((final result) {
if (result is ResultLoading) {
_notificationsAppImplementationSubject.add(Result.loading());
} else if (result is ResultError<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(Result.error(result.error));
} else if (result is ResultSuccess<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
Result.success(
result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null,
),
);
} else if (result is ResultCached<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
ResultCached(result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null),
);
}
});
_loadApps(); _loadApps();
} }
final _extraApps = ['notifications']; T? _findAppImplementation<T extends AppImplementation>(final String id) {
final matches = _filteredAppImplementations([id]);
if (matches.isNotEmpty) {
return matches.single as T;
}
List<AppImplementation> _filteredAppImplementations(final List<NextcloudApp> apps) { return null;
final appIds = apps.map((final a) => a.id).toList();
return _allAppImplementations.where((final a) => appIds.contains(a.id) || _extraApps.contains(a.id)).toList();
} }
List<AppImplementation> _filteredAppImplementations(final List<String> appIds) =>
_allAppImplementations.where((final a) => appIds.contains(a.id)).toList();
void _loadApps() { void _loadApps() {
_requestManager _requestManager
.wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>( .wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>(
@ -107,12 +137,14 @@ class AppsBloc extends $AppsBloc {
} }
final RequestManager _requestManager; final RequestManager _requestManager;
final CapabilitiesBloc _capabilitiesBloc;
final AccountsBloc _accountsBloc; final AccountsBloc _accountsBloc;
final Account _account; final Account _account;
final List<AppImplementation> _allAppImplementations; final List<AppImplementation> _allAppImplementations;
final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>(); final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>();
final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>(); final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>();
final _notificationsAppImplementationSubject = BehaviorSubject<Result<NotificationsApp?>>();
late final _activeAppSubject = BehaviorSubject<String?>(); late final _activeAppSubject = BehaviorSubject<String?>();
final Map<String, RxBlocBase> _blocs = {}; final Map<String, RxBlocBase> _blocs = {};
@ -128,6 +160,8 @@ class AppsBloc extends $AppsBloc {
@override @override
void dispose() { void dispose() {
unawaited(_appsSubject.close()); unawaited(_appsSubject.close());
unawaited(_appImplementationsSubject.close());
unawaited(_notificationsAppImplementationSubject.close());
unawaited(_activeAppSubject.close()); unawaited(_activeAppSubject.close());
for (final key in _blocs.keys) { for (final key in _blocs.keys) {
_blocs[key]!.dispose(); _blocs[key]!.dispose();
@ -142,6 +176,10 @@ class AppsBloc extends $AppsBloc {
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState() => _appImplementationsSubject; _mapToAppImplementationsState() => _appImplementationsSubject;
@override
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState() =>
_notificationsAppImplementationSubject;
@override @override
BehaviorSubject<String?> _mapToActiveAppIDState() => _activeAppSubject; BehaviorSubject<String?> _mapToActiveAppIDState() => _activeAppSubject;
} }

10
packages/neon/lib/src/blocs/apps.rxb.g.dart

@ -32,6 +32,11 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
late final BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> late final BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_appImplementationsState = _mapToAppImplementationsState(); _appImplementationsState = _mapToAppImplementationsState();
/// The state of [notificationsAppImplementation] implemented in
/// [_mapToNotificationsAppImplementationState]
late final BehaviorSubject<Result<NotificationsApp?>> _notificationsAppImplementationState =
_mapToNotificationsAppImplementationState();
/// The state of [activeAppID] implemented in [_mapToActiveAppIDState] /// The state of [activeAppID] implemented in [_mapToActiveAppIDState]
late final BehaviorSubject<String?> _activeAppIDState = _mapToActiveAppIDState(); late final BehaviorSubject<String?> _activeAppIDState = _mapToActiveAppIDState();
@ -48,6 +53,9 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> get appImplementations => BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> get appImplementations =>
_appImplementationsState; _appImplementationsState;
@override
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation => _notificationsAppImplementationState;
@override @override
BehaviorSubject<String?> get activeAppID => _activeAppIDState; BehaviorSubject<String?> get activeAppID => _activeAppIDState;
@ -56,6 +64,8 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState(); _mapToAppImplementationsState();
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState();
BehaviorSubject<String?> _mapToActiveAppIDState(); BehaviorSubject<String?> _mapToActiveAppIDState();
@override @override

3
packages/neon/lib/src/neon.dart

@ -56,9 +56,9 @@ import 'package:window_manager/window_manager.dart';
import 'package:xdg_directories/xdg_directories.dart' as xdg; import 'package:xdg_directories/xdg_directories.dart' as xdg;
part 'app.dart'; part 'app.dart';
part 'pages/account_settings.dart';
part 'pages/home.dart'; part 'pages/home.dart';
part 'pages/login.dart'; part 'pages/login.dart';
part 'pages/account_settings.dart';
part 'pages/nextcloud_app_settings.dart'; part 'pages/nextcloud_app_settings.dart';
part 'pages/settings.dart'; part 'pages/settings.dart';
part 'platform/abstract.dart'; part 'platform/abstract.dart';
@ -88,6 +88,7 @@ part 'utils/validators.dart';
part 'widgets/account_avatar.dart'; part 'widgets/account_avatar.dart';
part 'widgets/account_settings_tile.dart'; part 'widgets/account_settings_tile.dart';
part 'widgets/account_tile.dart'; part 'widgets/account_tile.dart';
part 'widgets/app_implementation_icon.dart';
part 'widgets/cached_api_image.dart'; part 'widgets/cached_api_image.dart';
part 'widgets/cached_image.dart'; part 'widgets/cached_image.dart';
part 'widgets/cached_url_image.dart'; part 'widgets/cached_url_image.dart';

84
packages/neon/lib/src/pages/home.dart

@ -30,11 +30,8 @@ class _HomePageState extends State<HomePage> {
_globalOptions = Provider.of<GlobalOptions>(context, listen: false); _globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account); _appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account);
_capabilitiesBloc = RxBlocProvider.of<AccountsBloc>(context).getCapabilitiesBloc(widget.account);
_capabilitiesBloc = CapabilitiesBloc(
Provider.of<RequestManager>(context, listen: false),
widget.account.client,
);
_capabilitiesBloc.capabilities.listen((final result) async { _capabilitiesBloc.capabilities.listen((final result) async {
if (result.data != null) { if (result.data != null) {
widget.onThemeChanged(result.data!.capabilities.theming!); widget.onThemeChanged(result.data!.capabilities.theming!);
@ -161,6 +158,16 @@ class _HomePageState extends State<HomePage> {
final appsError, final appsError,
final appsLoading, final appsLoading,
final _, final _,
) =>
StandardRxResultBuilder<AppsBloc, NotificationsApp?>(
bloc: _appsBloc,
state: (final bloc) => bloc.notificationsAppImplementation,
builder: (
final context,
final notificationsAppData,
final notificationsAppError,
final notificationsAppLoading,
final _,
) => ) =>
RxBlocBuilder<AppsBloc, String?>( RxBlocBuilder<AppsBloc, String?>(
bloc: _appsBloc, bloc: _appsBloc,
@ -342,27 +349,10 @@ class _HomePageState extends State<HomePage> {
onPressed: () { onPressed: () {
_appsBloc.setActiveApp(appImplementation.id); _appsBloc.setActiveApp(appImplementation.id);
}, },
icon: Stack( icon: AppImplementationIcon(
alignment: Alignment.bottomRight, appImplementation: appImplementation,
children: [ unreadCount: unreadCount,
Container( color: Theme.of(context).colorScheme.primary,
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
height: kAvatarSize,
width: kAvatarSize,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
), ),
), ),
); );
@ -501,6 +491,49 @@ class _HomePageState extends State<HomePage> {
], ],
), ),
actions: [ actions: [
if (notificationsAppData != null) ...[
StreamBuilder<int>(
stream: notificationsAppData.getUnreadCounter(_appsBloc),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton(
key: Key('app-${notificationsAppData.id}'),
icon: AppImplementationIcon(
appImplementation: notificationsAppData,
unreadCount: unreadCount,
color: unreadCount > 0
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
width: kAvatarSize * 2 / 3,
height: kAvatarSize * 2 / 3,
),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(notificationsAppData.name(context)),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!,
),
],
],
),
),
body: notificationsAppData.buildPage(context, _appsBloc),
),
),
);
},
);
},
),
],
IconButton( IconButton(
icon: IntrinsicWidth( icon: IntrinsicWidth(
child: AccountAvatar( child: AccountAvatar(
@ -573,6 +606,7 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
), ),
),
); );
} }
} }

47
packages/neon/lib/src/widgets/app_implementation_icon.dart

@ -0,0 +1,47 @@
part of '../neon.dart';
class AppImplementationIcon extends StatelessWidget {
const AppImplementationIcon({
required this.appImplementation,
this.unreadCount = 0,
this.color,
this.width = kAvatarSize,
this.height = kAvatarSize,
super.key,
});
final AppImplementation appImplementation;
final int unreadCount;
final Color? color;
final double width;
final double height;
@override
Widget build(final BuildContext context) => Stack(
alignment: Alignment.bottomRight,
children: [
Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
height: height,
width: width,
color: color,
),
),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
],
);
}
Loading…
Cancel
Save